9
\$\begingroup\$

It feels like there's quite a lot of code involved in order to manually build up an animation using the libGDX framework. In my specific case, I am creating a number of animations for a portrait view of a character. The character will do things like talk, blink, and laugh. There are a handful of different characters to worry about.

I would like to get some feedback on my approach. I'm hoping to simplify things as much as I can, but this is the best that I have come up with so far.

First, a texture atlas is created from a file. Then, the types from an enum are used to create a map of the types to the frames. I've removed all but one of the types just for brevity, but there is one for every single frame of animation.

PortraitType.java

public enum PortraitType {

    GOBLIN_TALK01("goblinTalkRight01", 106),
    GOBLIN_TALK02("goblinTalkRight02", 107),
    GOBLIN_TALK03("goblinTalkRight03", 108),
    GOBLIN_TALK04("goblinTalkRight04", 109),
    GOBLIN_TALK05("goblinTalkRight05", 110),
    GOBLIN_TALK06("goblinTalkRight06", 111),
    GOBLIN_TALK07("goblinTalkRight07", 112),
    GOBLIN_TALK08("goblinTalkRight08", 113),
    GOBLIN_TALK09("goblinTalkRight09", 114),
    GOBLIN_TALK10("goblinTalkRight10", 115),
    GOBLIN_TALK11("goblinTalkRight11", 116);

    public final String fileName;
    public final int id;

    private PortraitType(String fileName, int id) {
        this.fileName = fileName;
        this.id = id;
    }
}

LibGDXGame.java

private Map<PortraitType, TextureRegion> loadPortraitTextures() {
    Map<PortraitType, TextureRegion> textures =  new HashMap<PortraitType, TextureRegion>();
    TextureAtlas atlas = new TextureAtlas("rampartedPortraits01.atlas");
    for (PortraitType type : PortraitType.values()) {
        AtlasRegion region = atlas.findRegion(type.fileName);
        TextureRegion textureRegion = region;
        textures.put(type, textureRegion);
    }
    return textures;
}

After the map of all the frames is created, another map is created for each character that maps the type of animation to the animation itself.

PortraitAnimationType.java

public enum PortraitAnimationType {
    NONE,
    BLINK,
    TALK,
    TALK_BLINK,
    LAUGH,
    DEFEAT;
}

LibGDXGame.java

 private Map<PortraitAnimationType, Animation> loadGoblinAnimations() {
    Map<PortraitAnimationType, Animation> animations = new HashMap<PortraitAnimationType, Animation>();
    animations.put(PortraitAnimationType.NONE, AnimationLoader.goblinNoneAnimation(this.portraitTextures));
    animations.put(PortraitAnimationType.TALK, AnimationLoader.goblinTalkAnimation(this.portraitTextures));
    animations.put(PortraitAnimationType.BLINK, AnimationLoader.goblinBlinkAnimation(this.portraitTextures));
    animations.put(PortraitAnimationType.LAUGH, AnimationLoader.goblinLaughAnimation(this.portraitTextures));
    animations.put(PortraitAnimationType.TALK_BLINK, AnimationLoader.goblinTalkBlinkAnimation(this.portraitTextures));
    animations.put(PortraitAnimationType.DEFEAT, AnimationLoader.goblinDefeatAnimation(this.portraitTextures));
    return animations;
}

There is a separate class that is only used for creating the actual animations. I realize that it is possible to create animations automatically using an atlas, however there are some limitations to that approach. By doing it manually like this, I can influence exactly what frames will be played, and can do things like add a pause to the animation by repeating a frame.

AnimationLoader.java

public static Animation goblinTalkAnimation(Map<PortraitType, TextureRegion> textures) {
    TextureRegion[] regions = new TextureRegion[11];
    regions[0] = textures.get(PortraitType.GOBLIN_TALK01);
    regions[1] = textures.get(PortraitType.GOBLIN_TALK02);
    regions[2] = textures.get(PortraitType.GOBLIN_TALK03);
    regions[3] = textures.get(PortraitType.GOBLIN_TALK04);
    regions[4] = textures.get(PortraitType.GOBLIN_TALK05);
    regions[5] = textures.get(PortraitType.GOBLIN_TALK06);
    regions[6] = textures.get(PortraitType.GOBLIN_TALK07);
    regions[7] = textures.get(PortraitType.GOBLIN_TALK08);
    regions[8] = textures.get(PortraitType.GOBLIN_TALK09);
    regions[9] = textures.get(PortraitType.GOBLIN_TALK10);
    regions[10] = textures.get(PortraitType.GOBLIN_TALK11);
    return new Animation(1/8f, regions);
}

public static Animation goblinNoneAnimation(Map<PortraitType, TextureRegion> textures) {
    TextureRegion[] noneRegions = new TextureRegion[6];
    noneRegions[0] = textures.get(PortraitType.GOBLIN_TALK01);
    noneRegions[1] = textures.get(PortraitType.GOBLIN_TALK01);
    noneRegions[2] = textures.get(PortraitType.GOBLIN_TALK01);
    noneRegions[3] = textures.get(PortraitType.GOBLIN_TALK01);
    noneRegions[4] = textures.get(PortraitType.GOBLIN_TALK01);
    noneRegions[5] = textures.get(PortraitType.GOBLIN_TALK01);
    return new Animation(1/6f, noneRegions);
}

Finally, I've written a somewhat simple class that extends Image that handles playing the animations. As long as this image is part of a stage, it will automatically play the animations without needing to be manually updated each frame. I also programmatically handle whether the face of the character is facing left or right.

AnimatedPortrait.java

public class AnimatedPortrait extends Image {

    private float stateTime = 0;

    private PortraitAnimationType currentAnimation;

    private final Map<PortraitAnimationType, Animation> animations;

    private boolean paused  = false;

    private boolean isRightSide;

    public AnimatedPortrait(Map<PortraitAnimationType, Animation> animations, boolean isRightSide) {
        super(animations.get(PortraitAnimationType.NONE).getKeyFrame(0));

        this.animations = animations;

        this.isRightSide = isRightSide;

        this.currentAnimation = PortraitAnimationType.NONE;
    }

    public PortraitAnimationType getAnimation() {
        return this.currentAnimation;
    }

    public void setAnimation(PortraitAnimationType type) {
        this.stateTime = 0;
        this.currentAnimation = type;
    }

    private void cycleAnimations(float delta) {
        if (this.animations.get(currentAnimation).isAnimationFinished(this.stateTime)) {
            int current = currentAnimation.ordinal();
            current += 1;
            if (current >= PortraitAnimationType.values().length) {
                current = 0;
            }

            this.stateTime = 0;
            this.currentAnimation = PortraitAnimationType.values()[current];
        }
    }

    @Override
    public void act(float delta) {
        super.act(delta);
        if (this.paused) {
            return;
        }

        TextureRegion region = this.animations.get(this.currentAnimation).getKeyFrame(this.stateTime += delta, true);
        if (this.isRightSide &&
            !region.isFlipX()) {
            region.flip(true, false);
        }

        ((TextureRegionDrawable)getDrawable()).setRegion(region);

        this.cycleAnimations(delta);
    }
}

Here is a sample gif of the animation cycle for two characters:

castleparts portraits

And you can play an early demo of the game here:

Play Castleparts Demo

\$\endgroup\$
  • \$\begingroup\$ Have you noticed the vandal's background color goes from purple to black? \$\endgroup\$ – Phrancis Oct 26 '16 at 21:16
  • \$\begingroup\$ Yes, but thank you for pointing it out. None of the artwork is final yet. \$\endgroup\$ – bazola Oct 26 '16 at 21:58
  • \$\begingroup\$ If I have time, I'll write up an longer answer. Main improvement is removing the PortraitType type and use a GoblinTalkAnimation extends Animation that just contains a List of AnimationFrames \$\endgroup\$ – RobAu Dec 22 '16 at 10:49
4
+50
\$\begingroup\$

First of all, it's a pleasure watching your games evolve, keep it up!

Data management

Most of the code is about data. Creating all your objects with their behaviors programmatically is tedious, not practical. It's not easy to see all the data, as you have to jump between multiple classes to piece everything together. Probably it didn't seem that way in the beginning, but now it definitely is. I suggest to rework how the characters are built up, using a data driven approach.

As the first step, create factory and repository interfaces that will be in charge of materializing all the characters with their behaviors. The initial implementation can be the current code, transformed appropriately, still creating the characters fully programmatically.

As the second step, create an alternative implementation, creating the characters from flat files, for example, it could be CSV, or XML, it doesn't matter much. At some point later you might want to ditch that too, for example using a database backend or REST service backend, it doesn't matter, because thanks to the factory and repository interfaces, you will be able to replace the implementation without affecting the rest of the program.

Creating arrays

This is a very fragile way to populate an array:

TextureRegion[] noneRegions = new TextureRegion[6];
noneRegions[0] = textures.get(PortraitType.GOBLIN_TALK01);
noneRegions[1] = textures.get(PortraitType.GOBLIN_TALK01);
noneRegions[2] = textures.get(PortraitType.GOBLIN_TALK01);
noneRegions[3] = textures.get(PortraitType.GOBLIN_TALK01);
noneRegions[4] = textures.get(PortraitType.GOBLIN_TALK01);
noneRegions[5] = textures.get(PortraitType.GOBLIN_TALK01);
return new Animation(1/6f, noneRegions);

It's fragile, because the array size must match the assigned elements, the indexes must be correct, unique and compete. There are many possible points of human error. Better to write like this:

return new TextureRegion[] {
    textures.get(PortraitType.GOBLIN_TALK01),
    textures.get(PortraitType.GOBLIN_TALK01),
    // ..
};

Unused variables

The cycleAnimations method takes a delta parameter that is never used.

The paused field is never used.

Naming

The currentAnimation field is of type PortraitAnimationType, which is confusing considering there is also an Animation type, which is a close collaborator. Other methods that work with PortraitAnimationType also have just Animation in their names, further aggravating the confusion. It would be better if method names were more consistent with the types of objects they work with.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.