Monday, April 13, 2020

Tutorial 2.6: Congratulations, and Animations


Didn’t you say we were focussing on getting awesome stuff, rather than adding one engine file after another that doesn’t bring any direct results! True, true. Hang on, one or two more to go. You will see that we have created quite an engine already, making future work so much easier.

 We load (atlas) images into our game. We can find them by their idName – which is just the filename, which means YOU MUST USE UNIQUE FILENAMES in this tiny engine. But, the “EnEntitySprite” class we started with, still doesn’t know what to draw. We have to couple the Sprite with this Image, and pick a "Region" within the atlas.

But hold on. We can take the quick route here, but you'll get stuck again later once we want to animate our sprites. That means, playing multiple frames, like a cartoon. Or having multiple moves (walk, run, stand, poop, die, ...). So I wanted to anticipate on that. Looking in my crystal ball, I’d say we need a “SpriteAnimations” class... Abra kazam.





Sprite Attributes
Just like multiple entities can pick the same image, so they can use the same set of animations. But what exactly is an “animation” to begin with? We can describe it as;

·         IdName           Stand, Walk, run, punch, die01, die02, dieHard
·         FrameCount    How many frames will be played in a row?
·         Duration          How many seconds does the total animation take?
·         Next                Start playing another animation, replay itself (loop), or stick at the last frame



We may want to go even deeper, by adding a list of “Frames”. The data above assumes every frame takes stays in the picture for an equal amount of time. Ie, if “duration is 2 seconds”, and frameCount is 8#, then each frame will show for 2/8 = 0.25 seconds. If you want a finer control, you could make a FrameList instead, and give each its own time setting. 

Another thing you  can do with individual frames, is playing with the sprite origin (point of rotation/center), and size, in case it grows or something. In the"Punch" animation above, the right image is wider than the first one. That means you have to adjust the scale, otherwise Abobo (yes, that is his name) will shrink into a thinner version of hiimself at the second frame.

And what I did 16 years ago (-gosh time flies-) in a fighting game, was giving attributes. Some frames were “dangerous” while others weren’t. So in that punch animation above, the fist area in frame #2 would be marked as "dangerous"; if it overlaps another sprite, it would mean it got smacked. The first frame on the other hand is still safe to touch.


For now I don’t think we’ll need individual frames though (but I programmed it into the code below anyway). Two properties are still missing though:
·         GxImage             The (atlas) texture being used, that contains all the frames we need
·         TextureRegion[]  For each frame, we need to know the Atlas subregion
Notice that I did not use the built-in LibGDX animation class. Why not? Don’t know, I just didn’t.

public class GxAnimation {

    public class GxAnimationFrame {
        public TextureRegion    region;
        public float            duration;
        public GxAnimationFrame nextFrame;

        public GxAnimationFrame( float duration, String regionName, GxImage image )
        {   // get coordinates from the Atlas texture
            this.region     = image.findRegion( regionName );
            this.duration   = duration;
            this.nextFrame  = null;
        } // create

    } // GxAnimationFrame


    private class GxAnimationNext
    {
        GxAnimation next;
        float       chancePercent;

        public GxAnimationNext( GxAnimation next, float chancePercent )
        {
            this.next           = next;
            this.chancePercent  = chancePercent;
        }
    } // GxAnimationNext

//**********************************************
    String              idName;
    int                 frameCount;
    float               durationSecs;
    GxImage             image;
    GxAnimationFrame[]  frames;     // Atlas region coordinates for each frame + other props
    int                 nextCount;  // Amount of next animations, to play after this one is done
    GxAnimationNext[]   next;       // Play one of these after this animation is done

    public GxAnimation( String idName, int frameCount, float durationSecs, int nextCount, GxImage image )
    {
        this.idName         = idName;
        this.frameCount     = frameCount;
        this.durationSecs   = durationSecs;
        this.next           = null;
        this.image          = image;

        // Prepare region array, then find each frame
        if ( frameCount <= 1 ) {
            this.frames         = new GxAnimationFrame[ 1 ];
            this.frames[0]      = new GxAnimationFrame( this.durationSecs, this.idName, this.image );
        } else {
            float timePerFrame  = this.durationSecs / (float)this.frameCount;
            this.frames         = new GxAnimationFrame[ this.frameCount ];

            for (int i=0; i<this.frameCount; i++) {
                String num = String.valueOf(i);
                String frameName= this.idName+"_"+num;
                if ( i<10 )
                    frameName= this.idName+"0"+num; else
                    frameName= this.idName+num;
                this.frames[i]  = new GxAnimationFrame( timePerFrame, frameName, this.image );
            } // for i

            // Glue the frames to each other
            for (int i=0; i<this.frameCount - 1; i++) {
                this.frames[i].nextFrame = this.frames[i+1];
            } // for i
        }

        // Prepare Next array, but don't fill yet
        this.nextCount  = 0;
        this.next       = new GxAnimationNext[ nextCount ];
    } // create


    public void addNextAnimation( GxAnimation animation, float chancePercent )
    {
        if ( this.nextCount >= this.next.length ) return; // Cannot add more
        // Add next option
        this.next[ this.nextCount ] = new GxAnimationNext( animation, chancePercent );
        ++this.nextCount;
    } // addNextAnimation


    public GxAnimation getNext()
    {
        if ( this.nextCount < 1 ) return null;

        for ( int i=0; i<this.nextCount; i++) {
            float dice = CmMath.randF(0, 100);
            if ( dice <= this.next[i].chancePercent )
                return this.next[i].next;
        }

        return null; // Stick at the current frame
    } // getNext


    public GxAnimationFrame getFirstFrame()
    {
        return this.frames[0];
    } // getFirstFrame

} // GxAnimation
Jeez Rick, all that code. They say reading code is harder than writing code. What the hell did you cook up here? Ok, so there are 3 classes here: GxAnimation, made of 1 or more GxAnimationFrames. GxAnimation would describe a single "move", like "punch01", "sprint" or "Kamehameha!!". The GxAnimationFrame is about a single frame within the animation. Most important there, is how it is linked to a Region, a rect (coordinates) within the atlas picture. It also points to a next frame, if any, and tells how long this frame stays visible.

The other "GxAnimationNext" is sort of bonus feature. When an animation is done, we usuallly repeat (loop) or play another animation. Next refers to itself, or another animation. For example, after getting punched in the face, your next animation goes back to "stand". Or "cry" maybe. We can define multiple follow-ups, so the animation picks a random "next". This allows to create random Idle behavior  like:
* Stand  (90% chance)
* Scratch balls (5%)
* Light a cigarette (3%)
* Sneeze (2%)

A big chunk of the Constructor goes is about finding the Atlas region for each frame. In the Atlas image editor (see previous chapter), you can tag each region with its own idName. By default, this is just the original image name. For example, a walk animation could be made of "walk00.png, walk01.png, walk02.png". The idName we give to the Animation, should be "walk" in this case. Then the loop will try to find the region for each frame.



Using GxAnimation in the EnEntitySprite class
Got an Animation class now. Like other Resources, we only have to create them once, typically during the startup. If your ballerina has 5 different moves, we'll create 5 animations. Next, we can set a "currentAnimation" in our sprite class. Keep in mind this animation can change anytime. If you kick the ballerrina in her nuts, we should call "ballerinaEntity.setAnimation( resources.ani_BallerinaInAgony );".

The Sprite entity is also responsible for playing this animation. The GxAnimation doesn't play shit, it's just a collection of frames, follow-ups, and attributes. Each entity has to maintain its own animation state.
public class EnEntitySprite extends EnEntityBase {
    Sprite                          sprite;
    GxAnimation                     currentAnimation;
    GxAnimation.GxAnimationFrame    currentFrame;
    float                           frameDelta;

    public EnEntitySprite( GxImage img )
    {
        this.rotationAngle      = 0.f;
        this.sprite             = new Sprite( );
        this.currentAnimation   = null;
        this.currentFrame       = null;
    this.setSize( 2 );
        this.setPosition( 0,0 );
    } // create


    public void setAnimation( GxAnimation animation, boolean forceReplay )
    {
        if ( this.currentAnimation == animation  &&  forceReplay == false ) return; // Already playing
        this.currentAnimation   = animation;
        this.currentFrame       = this.currentAnimation.getFirstFrame();
        this.frameDelta         = 0.f;
        this.sprite.setRegion( this.currentFrame.region );
    } // setAnimation


    @Override
    public void update( float deltaSecs )
    {
        // Update Animation; get next animation if this one is done. And otherwise get the next frame,
        // or stick to the current one
        if ( this.currentAnimation != null ) {
            GxAnimation.GxAnimationFrame nextFrame = this.currentFrame;
            this.frameDelta += deltaSecs;

            if ( this.frameDelta >= this.currentFrame.duration ) {
                this.frameDelta = 0.f;
                nextFrame   = this.currentFrame.nextFrame;
                if ( nextFrame == null ) {
                    // Image done, pick another (if any)
                    GxAnimation nextAnimation   = this.currentAnimation.getNext();
                    if ( nextAnimation != null ) {
                        this.setAnimation( nextAnimation, true );
                    }
                }
            }

            // Frame changed
            if ( nextFrame != this.currentFrame  &&  nextFrame != null ) {
                this.frameDelta     = 0.f;
                this.currentFrame   = nextFrame;
                this.sprite.setRegion( nextFrame.region );
            }
        }
    } // update


    @Override
    public void render( Batch batch )
    {
        if ( this.currentFrame == null ) return;
        this.sprite.draw( batch );
    } // render

   ...

} // EnEntitySprite

So the "setAnimation" will reset the timer (unless the same animation was already playing), and fetch the first frame. The update function will keep a "frameDelta", a timer that increases until it passes the frame duration, then resets and pushes the next frame. When out of frames, we'll ask for a Next Animation -which could be a random one.  If there is no next animation, we just freeze. waiting until somebody will call the setAnimation() function again.



No comments:

Post a Comment