Minecraft Modding: Complex Custom Entity Models With Animation

Introduction


First of all, to understand the result of what I'm about to show, check out my video of my various models. I show some big cats, but especially show a python snake that is fully articulated.  I even have an elephant that has a trunk that moves, ears that flap, and that rears up when attacked. Anyway, check it out:


The python is what I show you how to make in this tutorial below.

This is something I've just gone through learning, and I am proud to say that I've got a fully animated slithering snake, and an elephant with flapping ears and articulated trunk.  Here are a few tips that might help others.

Techne Versus Modeling Directly in Java


First of all I found that using the Java exported directly from Techne is not a good idea.  Techne is useful though; I use Techne to visualize the model and animation, and especially use it for figuring out texture map, but ultimately I ended up coding the Java myself.  And the Java is pretty simple because it is simply a bunch of boxes (we're talking Minecraft(tm) after all).  Actually the biggest benefit of coding it manually is that you get a good understanding of how the rotation points and offsets really work.

Note there is also advanced modeling supported in Forge using .obj and wavefront models.  Personally though the Minecraft "feel" to me is about blocky models, and Java is pretty easy to use for blocky models.

Coordinate And Rotation Basics


TheGreyGhost has a good post explaining pitch, yaw and roll as well as the coordinate system used.

General Modeling Guidelines


Here are some ideas to think about:
  1. Your whole model will only be manageable if you orient it the right way -- I once spent a lot of time on one model which had head pointed 90 degrees to what Minecraft considers the front and while it could be fixed with some overall rotation it is much better to simply orient it correctly in the first place. You can check the ModelCow in Techne to ensure you have the right axis orientation  -- I believe it should be that negative Z direction is the direction the entity should face.
  2. Get your rotation point in proper place first, then figure out your offset. You can create two models that look the same but operate very differently/wrongly if you don't get the rotation points right.  The rotation point should be the joint for something like an arm, and should be the center for something like a body. In particular, make sure each joint is working properly before adding the next part. I have found that implementing some simple debug functions for spinning a part are immensely useful to help visualize that you have the rotation point and offsets correct. 
  3. Use a hierarchical model by associating blocks with the addChild() method. For examples, the ears and nose should be children of the head. Then when the head rotates they stay in proper place, and then the animations of the ears and nose can be programmed relatively (much easier to think about than using spatial transformation). 
  4. The fancy rendering stuff like animation goes into the render() method of the model class, not into your renderer class! The good thing about the render() method is that it gets the Entity passed to it, so you can (after casting to your custom entity) then test the fields and state of the entity to control your animations and other aspects of the rendering. 
  5. It is extremely useful to use the entity's tickExisted property to help control the cycles through an animation loop. I used the modulo (also called remainder function) to detect "every X ticks". For example, parEntity.ticksExisted%60==0 will be true for one tick every 3 seconds (there are 20 ticks per second). Even better, something like parEntity.ticksExisted%6 will work as an index through a six-step animation sequence. Note that ticksExisted is reset when a saved game is loaded which will cause all your entities to synchronize -- so you may want to add some random offset (I'll explain that later).
  6. Don't be afraid to "hard code" an animation loopJust like a cartoon artist, you only need a few set positions to give the illusion of motion. For example, for my snake I spent days trying to figure out a mathematical equation to control the slithering and then realized that with a very simple matrix of preset rotations I could achieve the same effect. 
  7. You may need to add public variables or access methods to your entities to control animations, in which case you need to ensure there are packets sent to client side (where rendering takes place) to control the animation. 
  8. A really important thing is that the default 0, 0, 0 position of an added box is on one corner, not in the center of the box. This means you will almost always want to offset by half the dimension of the sides for two (if you're rotating from an end) or three (if you're rotating around the middle) of the sides. 
  9. Don't worry about the texture until you get the model correct. You can work with no texture at all (in which case there is a pink-and-black checkered default texture), but I suggest working with a texture map that is filled in with a solid color -- this will let you test that you've got the texture asset in right location.  Changing your texture is a pain, so you want to get your model fully finished before you invest much time in texture creation.  
  10. For entity models, while testing your mod by playing it, use the F3+B shortcut to show the bounding boxes of your entity. It is really important to check that the actual bounding box is positioned correctly within the model. 

Example: Entity Serpent


The Model Class

Okay, here is my example ModelSerpent class for the EntitySerpent:

@SideOnly(Side.CLIENT)
public class ModelSerpent extends ModelBase
{
    public ModelRenderer head;
    public ModelRenderer tongue;
    public ModelRenderer body1;
    public ModelRenderer body2;
    public ModelRenderer body3;
    public ModelRenderer body4;
    public ModelRenderer body5;
    public ModelRenderer body6;
    public ModelRenderer body7;
    public ModelRenderer body8;
    public ModelRenderer body9;
    
    public int textureWidth = 64;
    public int textureHeight = 32;

    // create an animation cycle
    // for movement based animations you need to measure distance moved
    // and perform number of cycles per block distance moved.
    protected double distanceMovedTotal = 0.0D;
    // don't make this too large or animations will be skipped
protected static final double CYCLES_PER_BLOCK = 3.0D; protected int cycleIndex = 0; protected float[][] undulationCycle = new float[][] { { 45F, -45F, -45F, 0F, 45F, 45F, 0F, -45F }, { 0F, 45F, -45F, -45F, 0F, 45F, 45F, 0F }, { -45F, 90F, 0F, -45F, -45F, 0F, 45F, 45F }, { -45F, 45F, 45F, 0F, -45F, -45F, 0F, 45F }, { 0F, -45F, 45F, 45F, 0F, -45F, -45F, 0F }, { 45F, -90F, 0F, 45F, 45F, 0F, -45F, -45F }, }; public ModelSerpent() { head = new ModelRenderer(this, 0, 0); head.addBox(-2.5F, -1F, -5F, 5, 2, 5); head.setRotationPoint(0F, 23F, -8F); head.setTextureSize(textureWidth, textureHeight); setRotation(head, 0F, 0F, 0F); tongue = new ModelRenderer(this, 0, 13); tongue.addBox(-0.5F, -0.5F, -10F, 1, 1, 5); tongue.setRotationPoint(0F, 23F, -8F); tongue.setTextureSize(textureWidth, textureHeight); setRotation(tongue, 0F, 0F, 0F); body1 = new ModelRenderer(this, 20, 20); body1.addBox(-1.5F, -1F, -1F, 3, 2, 5); body1.setRotationPoint(0F, 23F, -8F); body1.setTextureSize(textureWidth, textureHeight); setRotation(body1, 0F, 0F, 0F); body2 = new ModelRenderer(this, 20, 20); body2.addBox(-1.5F, -1F, -1F, 3, 2, 5); body2.setRotationPoint(0F, 0F, 4F); body2.setTextureSize(textureWidth, textureHeight); body1.addChild(body2); setRotation(body2, 0F, undulationCycle[0][0], 0F); body3 = new ModelRenderer(this, 20, 20); body3.addBox(-1.5F, -1F, -1F, 3, 2, 5); body3.setRotationPoint(0F, 0F, 4F); body3.setTextureSize(textureWidth, textureHeight); setRotation(body3, 0F, undulationCycle[0][1], 0F); body2.addChild(body3); body4 = new ModelRenderer(this, 20, 20); body4.addBox(-1.5F, -1F, -1F, 3, 2, 5); body4.setRotationPoint(0F, 0F, 4F); body4.setTextureSize(textureWidth, textureHeight); setRotation(body4, 0F, undulationCycle[0][2], 0F); body3.addChild(body4); body5 = new ModelRenderer(this, 20, 20); body5.addBox(-1.5F, -1F, -1F, 3, 2, 5); body5.setRotationPoint(0F, 0F, 4F); body5.setTextureSize(textureWidth, textureHeight); setRotation(body5, 0F, undulationCycle[0][3], 0F); body4.addChild(body5); body6 = new ModelRenderer(this, 20, 20); body6.addBox(-1.5F, -1F, -1F, 3, 2, 5); body6.setRotationPoint(0F, 0F, 4F); body6.setTextureSize(textureWidth, textureHeight); setRotation(body6, 0F, undulationCycle[0][4], 0F); body5.addChild(body6); body7 = new ModelRenderer(this, 30, 0); body7.addBox(-1F, -1F, -1F, 2, 2, 5); body7.setRotationPoint(0F, 0F, 4F); body7.setTextureSize(textureWidth, textureHeight); setRotation(body7, 0F, undulationCycle[0][5], 0F); body6.addChild(body7); body8 = new ModelRenderer(this, 30, 0); body8.addBox(-1F, -1F, -1F, 2, 2, 5); body8.setRotationPoint(0F, 0F, 4F); body8.setTextureSize(textureWidth, textureHeight); setRotation(body8, 0F, undulationCycle[0][6], 0F); body7.addChild(body8); body9 = new ModelRenderer(this, 22, 12); body9.addBox(-0.5F, -0.5F, -1F, 1, 1, 5); body9.setRotationPoint(0F, 0F, 4F); body9.setTextureSize(textureWidth, textureHeight); setRotation(body9, 0F, undulationCycle[0][7], 0F); body8.addChild(body9); } /** * Sets the models various rotation angles then renders the model. */ @Override public void render(Entity parEntity, float parTime, float parSwingSuppress, 
          float par4, float parHeadAngleY, float parHeadAngleX, float par7)
    {
        // best to cast to actual expected entity, to allow access to custom fields 
        // related to animation
        renderSerpent((EntitySerpent) parEntity, parTime, parSwingSuppress, par4, 
              parHeadAngleY, parHeadAngleX, par7);
    }
    
    public void renderSerpent(EntitySerpent parEntity, float parTime, float parSwingSuppress, 
          float par4, float parHeadAngleY, float parHeadAngleX, float par7)
    {
        setRotationAngles(parTime, parSwingSuppress, par4, parHeadAngleY, parHeadAngleX, 
              par7, parEntity);

        // scale the whole thing for big or small entities
        GL11.glPushMatrix();
        GL11.glScalef(parEntity.getScaleFactor(), parEntity.getScaleFactor(), 
              parEntity.getScaleFactor());

        if (this.isChild)
        {
            float childScaleFactor = 0.5F;
            GL11.glPushMatrix();
            GL11.glScalef(1.0F * childScaleFactor, 1.0F * childScaleFactor, 1.0
                  * childScaleFactor);
            GL11.glTranslatef(0.0F, 24.0F * par7, 0.0F);
            head.render(par7);
            // flick tongue occasionally
if (parEntity.ticksExisted%60==0 && parSwingSuppress <= 0.1F) {tongue.render(par7);} 
            body1.render(par7); // all rest of body are children of body1
            GL11.glPopMatrix();
        }
        else
        {
            head.render(par7);
            // flick tongue occasionally
if (parEntity.ticksExisted%60==0 && parSwingSuppress <= 0.1F) {tongue.render(par7);} body1.render(par7); // all rest of body are children of body1 } // don't forget to pop the matrix for overall scaling GL11.glPopMatrix(); } @Override public void setRotationAngles(float parTime, float parSwingSuppress, float par3, 
          float parHeadAngleY, float parHeadAngleX, float par6, Entity parEntity)
    {
        // animate if moving        
        updateDistanceMovedTotal(parEntity);
        cycleIndex = (int) ((getDistanceMovedTotal(parEntity)*CYCLES_PER_BLOCK)
              %undulationCycle.length);
        // DEBUG
        System.out.println("ModelSerpent setRotationAngles(), distanceMoved ="
              +getDistanceMovedTotal(parEntity)+", cycleIndex ="+cycleIndex);
        body2.rotateAngleY = degToRad(undulationCycle[cycleIndex][0]) ;
        body3.rotateAngleY = degToRad(undulationCycle[cycleIndex][1]) ;
        body4.rotateAngleY = degToRad(undulationCycle[cycleIndex][2]) ;
        body5.rotateAngleY = degToRad(undulationCycle[cycleIndex][3]) ;
        body6.rotateAngleY = degToRad(undulationCycle[cycleIndex][4]) ;
        body7.rotateAngleY = degToRad(undulationCycle[cycleIndex][5]) ;
        body8.rotateAngleY = degToRad(undulationCycle[cycleIndex][6]) ;
        body9.rotateAngleY = degToRad(undulationCycle[cycleIndex][7]) ;  
    }
    
    protected void updateDistanceMovedTotal(Entity parEntity) 
    {
        distanceMovedTotal += parEntity.getDistance(parEntity.prevPosX, parEntity.prevPosY, 
              parEntity.prevPosZ);
    }
    
    protected double getDistanceMovedTotal(Entity parEntity) 
    {
        return (distanceMovedTotal);
    }

    protected float degToRad(float degrees)
    {
        return degrees * (float)Math.PI / 180 ;
    }
    
    protected void setRotation(ModelRenderer model, float rotX, float rotY, float rotZ)
    {
        model.rotateAngleX = degToRad(rotX);
        model.rotateAngleY = degToRad(rotY);
        model.rotateAngleZ = degToRad(rotZ);        
    }

    // spin methods are good for testing and debug rotation points and offsets in the model
    protected void spinX(ModelRenderer model)
    {
        model.rotateAngleX += degToRad(0.5F);
    }
    
    protected void spinY(ModelRenderer model)
    {
        model.rotateAngleY += degToRad(0.5F);
    }
    
    protected void spinZ(ModelRenderer model)
    {
        model.rotateAngleZ += degToRad(0.5F);
    }
}

Explanation


Animation Cycles

The first part that I want to bring attention to is the animation cycle I hard-coded:

// create an animation cycle
// for movement based animations you need to measure distance moved
// and perform number of cycles per block distance moved.
protected double distanceMovedTotal = 0.0D;
// don't make this too large or animations will be skipped
protected static final double CYCLES_PER_BLOCK = 3.0D; 
protected int cycleIndex = 0;
protected float[][] undulationCycle = new float[][]
{
    {  45F, -45F, -45F,   0F,  45F,  45F,   0F, -45F },
    {   0F,  45F, -45F, -45F,   0F,  45F,  45F,   0F },
    { -45F,  90F,   0F, -45F, -45F,   0F,  45F,  45F },
    { -45F,  45F,  45F,   0F, -45F, -45F,   0F,  45F },
    {   0F, -45F,  45F,  45F,   0F, -45F, -45F,   0F },
    {  45F, -90F,   0F,  45F,  45F,   0F, -45F, -45F },
};

Basically, each row is a step in the animation and each column is one of the snake's body parts.  I came up with this table by simply drawing a snake (in each of its positions of slithering) on a piece of paper and figuring out the relative angles between parts (they are relative because I use child parts, as explained below).

Using Part Hierarchy (Child Parts) To Make The Model Easy To Manage

Note that each body part in the snake is child of the previous, so that all the parts remain connected. You need to create one part that you consider the main part of the model, for most entities I would use the body (or body1 in the case of my snake with multiple parts to its body).

Then for each body part, I use the following code structure to create a hierarchy of parts using the addChild() method:

body1 = new ModelRenderer(this, 20, 20);
body1.addBox(-1.5F, -1F, -1F, 3, 2, 5);
body1.setRotationPoint(0F, 23F, -8F);
body1.setTextureSize(textureWidth, textureHeight);
setRotation(body1, 0F, 0F, 0F);
body2 = new ModelRenderer(this, 20, 20);
body2.addBox(-1.5F, -1F, -1F, 3, 2, 5);
body2.setRotationPoint(0F, 0F, 4F);
body2.setTextureSize(textureWidth, textureHeight);
body1.addChild(body2);
setRotation(body2, 0F, degToRad(undulationCycle[cycleIndex][0]), 0F);
body3 = new ModelRenderer(this, 20, 20);
body3.addBox(-1.5F, -1F, -1F, 3, 2, 5);
body3.setRotationPoint(0F, 0F, 4F);
body3.setTextureSize(textureWidth, textureHeight);
setRotation(body3, 0F, degToRad(undulationCycle[cycleIndex][1]), 0F);
body2.addChild(body3);

Special Tip For Converting Techne Models Into Hierarchical Models

If you have created a complex model in Techne, you may find that it can be a lot of work to convert it into a hierarchical model.  This is because you have to take all the rotation points and make them relative, meaning subtracting out the parent's rotation point.  If you have a lot of parts, this is time consuming and error prone.  Instead you can use a simple helper function method which I call convertToChild() to do this for you: Jabelar's convertToChild() Tutorial.

Explanation Of The ModelRenderer Methods

For people that may not know, if you want to understand the Java parameters or check them against the modeling info in Techne:
 - the parameters in the ModelRenderer() constructor are the texture offset
 - the first three parameters in the addBox() method are the offset x, y, z
 - the second three parameters in the addBox() method are the dimensions x, y, z

When To Use Child Hierarchy

The next important thing is knowing when to use children boxes.  If you have anything that should move with something else but also have its own rotation, then I strongly suggest making it a child.  Otherwise, you have to do a lot of trigonometry to keep it connected.

(Note that if you don't want to move the piece separately, you can simply add multiple boxes to one ModelRenderer just with another addBox() method without an additional model renderer.  I do this with the noses for the tigers and lions in the video.)

In any case, something like an ear should NEVER (if you want to remain sane) be a fully separate box, but rather should be either a child or at least added to the head.  Then when you rotate the head, it will all move together.

In the case of the snake, I took the hierarchy to the extreme -- each segment of the snake's body is a child of the previous.  So all I have to do is set the rotations (which are relative) of each body segment and then just render only the body (which contains all the segments).

You Only Have To Render The Parent Parts


Note that I render the head and the body, but don't render each body part.  You only need to render the parent and all children will be automatically rendered.  

Warning: The only downside to parts being in a parent-child hierarchy is you can't do any specific GL11 operations between each part being rendered (for example sometimes with overlapping parts it is nice to scale them slightly to avoid render flicker, or also sometimes you want to put special blend modes onto certain parts).

More About Animation


Knowing When The Entity Is Moving

Entities have built-in public fields for both the current position (posXposYposZ) and the position in the previous tick (prevPosXprePosYprePosZ).  So depending on how you want your animations to work with movement you can check for movement and control animations.  For the snake, I wanted to have it animate whenever it was moving along the ground so I check for change in location.  If you wanted your animation affected only by vertical movement (like maybe if it were falling) you could check for that and animate accordingly.  So I create some helper fields and methods.  For fields I want to track total distance covered, and I want a constant for how many animation cycles I want to go through while the entity moved the distance of one block:

protected double distanceMovedTotal = 0.0D;
protected static final double CYCLES_PER_BLOCK = 3.0D; 

I also created helper functions to update and get the distanceMovedTotal;

protected void updateDistanceMovedTotal(Entity parEntity) 
{
    distanceMovedTotal += parEntity.getDistance(parEntity.prevPosX, parEntity.prevPosY, parEntity.prevPosZ);
}
    
protected double getDistanceMovedTotal(Entity parEntity) 
{
    return (distanceMovedTotal);
}

Other Entity Fields For Controlling Animation

For movement, there is actually a parameter (the second float passed into the setRotationAngles() method) which the comments say represents an amount for arm and leg to swing for bipedal models. So it is sort of a movement indicator.  In fact, to make the serpents tongue only flick out if the serpent is not moving I check that parameter (which I call parSwingAmount).

There are some additional vanilla fields that you may have fun using to control your animations. For example, I have an elephant entity that will raise its trunk if it is in the water.  So I test the isInWater() method and set the trunk angle accordingly.

Some of my animals have children where the model is different.  For example, the trunk of my baby elephant is only one piece instead of two.  So I use the isChild() method to control that.   I actually create a whole different head ModelRenderer for the child head, and I even have a modified animation cycle since the proportions are different.  Anyway, my point is that you can do quite complex logic to set the angles and to selectively render (even swap in an entirely different head) if you check some of the entity variables and process accordingly.

You may want to create custom fields for controlling custom animations.  For example my elephant will rear up when attacked and so I have counter field that indicates where it is in the rearing cycle.  Note though that rendering happens on the client side so if you have a state variable that is set on server that you need to control an animation then you have to send a packet from server to client.  You can see my tutorial on that here: Jabelar's custom packets for Entity sync tutorial

Animating Using setRotationAngles()

Okay, the next part I'm really proud of.  It is really easy and really powerful animating technique.  Let's focus in on the setRotationAngles() method where the animation happens:

@Override
public void setRotationAngles(float parTime, float parSwingSuppress, float par3, float parHeadAngleY, float parHeadAngleX, float par6, Entity parEntity)
{
    // animate if moving     
    updateDistanceMovedTotal(parEntity);
    cycleIndex = (int) ((getDistanceMovedTotal(parEntity)*CYCLES_PER_BLOCK)%undulationCycle.length);
    body2.rotateAngleY = degToRad(undulationCycle[cycleIndex][0]) ;
    body3.rotateAngleY = degToRad(undulationCycle[cycleIndex][1]) ;
    body4.rotateAngleY = degToRad(undulationCycle[cycleIndex][2]) ;
    body5.rotateAngleY = degToRad(undulationCycle[cycleIndex][3]) ;
    body6.rotateAngleY = degToRad(undulationCycle[cycleIndex][4]) ;
    body7.rotateAngleY = degToRad(undulationCycle[cycleIndex][5]) ;
    body8.rotateAngleY = degToRad(undulationCycle[cycleIndex][6]) ;
    body9.rotateAngleY = degToRad(undulationCycle[cycleIndex][7]) ;  
}

All I'm doing is cycling through the hard-coded array of angles for the animation.  There is no trigonometry at all to calculate the rotation angles!  Since all the rotations are relative (the body parts are children of preceding part) and because I figured out the right angles through simply drawing what I wanted on a piece of paper (you can also position it in Techne), it just cycles through them.

Explanation Of The cycleIndex

Animations are usually updated every tick.  Because this example case is of a slithering serpent the animation is meant to be associated with movement.  So I keep track of distance covered using a variable called distanceMovedTotal.  This has the advantage of allowing the animation to change speed as the movement of the entity changes speed.  distanceMovedTotal doesn't go back to zero, so I use the modulo operator (also called the remainder operator) represented by % to limit the cycleIndex to stay within the length of the animation array. Modulo is a very easy way to create a resetting counter from a continuous one.

Other Ways Of Driving the cycleIndex Depending On Your Need

You may have an animation that doesn't relate to entity movement, such as something that continuously moves. In that case you can create your own counter that increments every tick -- for this I usually use the Entity class' built-in ticksExisted field.  For example I used this to make the serpent's tongue come out:

// flick tongue occasionally
if (parEntity.ticksExisted%60==0 && parSwingAmount <= 0.1F) {tongue.render(par7);}

Preventing Animation Synchronization

There is one small problem with the tick counters (whether a custom one or the built-in ticksExisted) though -- they get reset to 0 when you load the game. This is a problem for us because it means all loaded entities will have same value for the cycleIndex, so then all your animations will be synchronized. In other words, all birds would flap at same moment, all elephants' trunks would swing at the same moment, and so forth.  Unless your entity is a soldier or something that you want synchronized, you probably don't want perfect synchronization between entities.  So I add the entityID to the formula for cycleIndex before applying the modulo operator .  This creates some "randomness".  Note that entities created in sequence might still not appear random enough so I multiply by 7 or similar.  If that isn't enough randomness you could initialize your counter to a random number. (Don't recalculate the random number every tick or that will break your animation sequence! Rather calculate it once during initialization). 

Slowing Down The Animation

There are two easy ways of slowing down the animation.  You can of course make more rows in your animation matrix, or you can apply some constant multiplier to control this.  In my case above, I want the slithering of the snake to relate to the amount of ground covered during movement, so I created a constant called CYCLES_PER_BLOCK.  I adjust this until I get the effect I want.  Just make sure that you don't use a multiplier that causes cycleIndex to skip steps in the animation.

Helper Functions


Lastly I have a couple helper functions.  degToRad() is simply a float version of simple converter -- I just find a lot of math utils require casting if you're working in floats.  But certainly you can use another math helper function, or if you can think in radians then you're a better mind than me!

The spinX()etc. methods I find extremely important during the creative and debug process although they are not used in my final code.  I use them to debug my model.  I take each part in turn, and use the spin on them in the render (while disabling other rotations).  This creates an obvious visual check that your rotation point and offsets are correct.

Conclusion


There are three things I would like people to take from this tutorial:  don't just use Techne blindly but learn and modify the Java code it generates, use child parts to keep your model connected and the math simple, and lastly do animations just like a real animator (by figuring out each position and cycling through them).

I especially want to promote the idea of using an array of angles to control your animation -- after days of working through complex trigonometry I suddenly realized you don't really need math to animate something through a loop with only a few steps!

Please comment if you see any errors or suggestions for improvement!

2 comments:

  1. Hey Jabelar,

    I don't know if this is the best way to get in contact with you, but I'd like to ask about Minecraft modding animations.

    I've recently released an alpha version of the Obsidian Animator, a piece of software that allows you to make animations for models, and these animations can be used within mods to animate the entity models in game. I'm a little bit out of touch with the Minecraft community at the moment, and I'm not really sure what the standard modelling/animating technique is nowadays. Previously I've used Techne and done animations similar to the ones outline in your tutorial.

    Basically, I think the Obsidian Suite (Animator and API) has the potential to be the new mainstream way to make animations for models, I just don't know the best way to get it there. The project is completely open source, and I can certainly write the code, but I need someone on board who has more modding experience than I and is up to date with the community. If this at all interests you, please send me an email at dabigjoe97@gmail.com. Thank you for your time, I hope to hear from you soon!

    Cheers,
    DaBigJoe

    ReplyDelete
  2. Hey,

    I got lost right around the part where you talk about animation synchronization prevention. You said that you add some randomness to the animation by adding the entityID to the formula before the modulo operator but I don't see that in the code, I know that I'm probably just overlooking it, I'm just new to modding and only know coding basics.

    ReplyDelete