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