Saturday, February 9, 2013

Sprite Animation with libgdx and Artemis (Level 2)

After finishing Spaceship warrior, I felt like there were a few more concepts I had to get down.  The first piece I decided to tackle was implementing a SpriteAnimationSystem.  There are a few useful tools in libgdx to accomplish this.

First, when TextuerPacker2 creates a TextureAtlas, it creates a file mapping the file-name to the appropriate region.  If you have a series of images named imageName_0.png, imageName_1.png, ..., imageName_100.png it interprets the _x as an index, and refers to them all by the name imageName.  When there is no number, the atlas just assigns a default index of -1.  But with the numbers, you get indices corresponding to each file.

When you call
atlas.findRegion("imageName");
it returns only the last image of the series.  But if you call
atlas.findRegions("imageName");
it returns an array of all of them, indexed appropriately..  This array can be passed into libgdx's Animation class to create an object that will cycle through all the images in the set.  My idea was to create a new Component called SpriteAnimation which would hold this animation, and SpriteAnimationSystem which would update the Sprite component based on the status of SpriteAnimation.

This way, the SpriteRenderSystem could be used to render everything with just a single static Sprite, and things with SpriteAnimation because all SpriteRenderSystem would ever call was the Sprite class.

First, I did a Google search to find a sprite sheet I could use for the animation, and I came across a game in development called Hero of Allacrost, staring the young knight Claudius.  I found a spritesheet for Claudius and lifted a few frames, naming them warrior_2 through warrior_6 (I started off of 0 just to see what would happen - it worked fine!)


 
Each image is 32x64, and they line up to look like continuous walking.

To understand my implementation, I want to first focus on the TextureRegion class.  TextureRegions have a reference to a Texture, along with rectangle coordinates specifying which part of that Texture to use.  These coordinates are stored in 2 different formats: float u, u2 refer to the x coordinates as a percent of overall texture width.  That is, if a Texture is 100 pixels wide, and the TextureRegion runs horizontally through pixel 35 and pixel 73, u would be 0.35f and u2 would be 0.73f.  float v, v2 do likewise for the vertical component.

On the other hand, int x, y, width, height store the same information in raw pixel counts.  In the example above, x would be 35 and width would be 73-35 = 38. 

The first major change I had to make was to SpriteRenderSystem and Sprite.  In Spaceship Warrior, Sprites only had a name, but SpriteRenderSystem had a bag which assigned a TextureRegion to each entity.  This wouldn't work anymore because the Sprite class itself had to hold its own TextureRegion so the AnimatedSpriteSystem could update it.

Even worse, the Sprites would now only hold a refenece to the actual TextureRegion created from the actual TextureAtlas.  That means when one Sprite updated "its" TextureRegion, it would actually mess with all of them.

To get around this, I used the coordinate information talked about above, and each Sprite had it's own int x, y, width, and height.  Instead of touching the TextureRegion itself, AnimatedSpriteSystem would instead change the Sprite's coordinate information, and the Sprite would update the TextureRegion right before it was drawn.

Here is my implementation:
public class Sprite extends Component {
 
 public Sprite(String name) {
  this.name = name;
 }
 
 
 public TextureRegion region;
 public String name;
 public float r,g,b,a,scaleX,scaleY,rotation;

 public int x, y, width, height;
}

package com.gamexyz.components;
public class SpriteAnimation extends Component {
 
 public Animation animation;
 public float stateTime;
 public float frameDuration;
 public int playMode;
 
 public TextureRegion getFrame() {
  return animation.getKeyFrame(stateTime);
 }
 
}

public class SpriteAnimationSystem extends EntityProcessingSystem {
 @Mapper ComponentMapper<Sprite> sm;
 @Mapper ComponentMapper<SpriteAnimation> sam;
 
 @SuppressWarnings("unchecked")
 public SpriteAnimationSystem() {
  super(Aspect.getAspectForAll(Sprite.class, SpriteAnimation.class));
 }

 @Override
 protected void process(Entity e) {

  Sprite sprite = sm.get(e);
  SpriteAnimation anim = sam.get(e);
  
  anim.stateTime += world.getDelta();

  TextureRegion region = anim.getFrame();
  sprite.x = region.getRegionX();
  sprite.y = region.getRegionY();
  sprite.width = region.getRegionWidth();
  sprite.height = region.getRegionHeight();
 }

 @Override
 protected boolean checkProcessing() {
  return true;
 }
}

public class SpriteRenderSystem extends EntitySystem {
 @Mapper ComponentMapper<position> pm;
 @Mapper ComponentMapper<sprite> sm;
 @Mapper ComponentMapper<spriteanimation> sam;
 
 private OrthographicCamera camera;
 private SpriteBatch batch;
 
 private TextureAtlas atlas;
 
 private List<entity> sortedEntities;
 
 @SuppressWarnings("unchecked")
 public SpriteRenderSystem(OrthographicCamera camera) {
  super(Aspect.getAspectForAll(Position.class, Sprite.class));
  this.camera = camera;
 }
 
 @Override
 protected void initialize() {
  batch = new SpriteBatch();
  
  atlas = new TextureAtlas(Gdx.files.internal("textures/pack.atlas"),Gdx.files.internal("textures"));
  sortedEntities = new ArrayList<entity>();
 }

 @Override
 protected boolean checkProcessing() {
  return true;
 }

 @Override
 protected void processEntities(ImmutableBag<entity> entities) {
  for (Entity e : sortedEntities) {
   process(e);
  }
 }
 
 @Override
 protected void begin() {
  batch.setProjectionMatrix(camera.combined);
  batch.begin();
 }
 
 protected void process(Entity e) {
  if (pm.has(e)) {
   Position position = pm.getSafe(e);
   Sprite sprite = sm.get(e);
   
   TextureRegion spriteRegion = sprite.region;
   batch.setColor(sprite.r, sprite.g, sprite.b, sprite.a);

   int width = spriteRegion.getRegionWidth();
   int height = spriteRegion.getRegionHeight();
   
   sprite.region.setRegion(sprite.x, sprite.y, width, height);
   
   float posX = position.x - (spriteRegion.getRegionWidth() / 2 * sprite.scaleX);
   float posY = position.y - (spriteRegion.getRegionHeight() / 2 * sprite.scaleX);
   batch.draw(spriteRegion, posX, posY, 0, 0, spriteRegion.getRegionWidth(), spriteRegion.getRegionHeight(), sprite.scaleX, sprite.scaleY, sprite.rotation);
  }
 }
 
 @Override
 protected void end() {
  batch.end();
 }
 
 @Override
 protected void inserted(Entity e) {
  Sprite sprite = sm.get(e);
  sortedEntities.add(e);
  TextureRegion reg = atlas.findRegion(sprite.name);
  sprite.region = reg;
  sprite.x = reg.getRegionX();
  sprite.y = reg.getRegionY();
  sprite.width = reg.getRegionWidth();
  sprite.height = reg.getRegionHeight();
  if (sam.has(e)) {
   SpriteAnimation anim = sam.getSafe(e);
   anim.animation = new Animation( anim.frameDuration, atlas.findRegions(sprite.name), anim.playMode);
  }
  
  Collections.sort(sortedEntities, new Comparator<entity>() {
   @Override
   public int compare(Entity e1, Entity e2) {
    Sprite s1 = sm.get(e1);
    Sprite s2 = sm.get(e2);
    return s1.layer.compareTo(s2.layer);
   }
  });
 }
 
 @Override
 protected void removed(Entity e) {
  sortedEntities.remove(e);
 }
}
I completely abandoned the regionsByName bag, and even the regions HashMap which mapped "names" to regions, instead I rely entirely on atlas.getRegion(...).  This is potentially slow, and if I ever need to create a lot of entities at once it may be helpful to recreate the HashMap - especially for the static sprites.  For instance, if I created a particle explosion effect, I may do that down the line.  But for now this seems fine.

My least favorite part comes in lines 79-82.  The SpriteRenderSystem doesn't necessarily require its entities have SpriteAnimation, so I had to carefully check before referencing it.  The reason I didn't include this in a different system, which would have been preferable, is that this is where my TextureAtlas lives.  Without having that to point to, I can't instantiate my Animation.

From that, SpriteAnimationSystem updates the animated sprites x, y, etc, and it gets drawn properly to the screen.  I created an EntityFactory that will make a character with the Claudius animation, and I added a bunch of them.  Copying over my HudRenderingSystem from the previous project let me see that FPS did okay even with thousands of such entities being processed and rendered onscreen.  Furthermore, I made them all start out of step by giving them a random stateTime.  stateTime is the thing that Animation uses to calculate which frame to work with.  For fun, I also added a few static Sprites to verify that they would alongside the animated ones.

int playMode in SpriteAnimation refers to the playMode in libgdx's Animation class.  I like to use Animation.LOOP_PINGPONG for the Claudius animation, which means that once it reaches the last frame, it turns around and loops through them in reverse, back and forth.  This is particularly convenient for a walking animation.  Unfortunately, PINGPONG has a quirk that the endpoint frames last for twice as long.  This ruined our aesthetic in my opinion, and seems extremely silly!  I downloaded the raw code from their github repository here to see what was going on.

Unsatisfied with the way they did things, I created my own Animation class by totally stealing theirs and changing one tiny piece of the code.  I put it in a new package called com.gamexyz.custom, and here is what it looks like:
/*******************************************************************************
 * Copyright 2011 See AUTHORS file.
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *   http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 ******************************************************************************/

package com.gamexyz.custom;

import com.badlogic.gdx.graphics.g2d.TextureRegion;
import com.badlogic.gdx.math.MathUtils;
import com.badlogic.gdx.utils.Array;

/** * An Animation stores a list of {@link TextureRegion}s representing an animated sequence, e.g. for running or jumping. Each
 * region of an Animation is called a key frame, multiple key frames make up the animation.
 * 


 * 
 * @author mzechner */
public class Animation {
 public static final int NORMAL = 0;
 public static final int REVERSED = 1;
 public static final int LOOP = 2;
 public static final int LOOP_REVERSED = 3;
 public static final int LOOP_PINGPONG = 4;
 public static final int LOOP_RANDOM = 5;

 final TextureRegion[] keyFrames;
 public final float frameDuration;
 public final float animationDuration;

 private int playMode = NORMAL;

 /** Constructor, storing the frame duration and key frames.
  * 
  * @param frameDuration the time between frames in seconds.
  * @param keyFrames the {@link TextureRegion}s representing the frames. */
 public Animation (float frameDuration, Array keyFrames) {
  this.frameDuration = frameDuration;
  this.animationDuration = keyFrames.size * frameDuration;
  this.keyFrames = new TextureRegion[keyFrames.size];
  for (int i = 0, n = keyFrames.size; i < n; i++) {
   this.keyFrames[i] = keyFrames.get(i);
  }

  this.playMode = NORMAL;
 }

 /** Constructor, storing the frame duration, key frames and play type.
  * 
  * @param frameDuration the time between frames in seconds.
  * @param keyFrames the {@link TextureRegion}s representing the frames.
  * @param playType the type of animation play (NORMAL, REVERSED, LOOP, LOOP_REVERSED, LOOP_PINGPONG, LOOP_RANDOM) */
 public Animation (float frameDuration, Array keyFrames, int playType) {

  this.frameDuration = frameDuration;
  this.animationDuration = keyFrames.size * frameDuration;
  this.keyFrames = new TextureRegion[keyFrames.size];
  for (int i = 0, n = keyFrames.size; i < n; i++) {
   this.keyFrames[i] = keyFrames.get(i);
  }

  this.playMode = playType;
 }

 /** Constructor, storing the frame duration and key frames.
  * 
  * @param frameDuration the time between frames in seconds.
  * @param keyFrames the {@link TextureRegion}s representing the frames. */
 public Animation (float frameDuration, TextureRegion... keyFrames) {
  this.frameDuration = frameDuration;
  this.animationDuration = keyFrames.length * frameDuration;
  this.keyFrames = keyFrames;
  this.playMode = NORMAL;
 }

 /** Returns a {@link TextureRegion} based on the so called state time. This is the amount of seconds an object has spent in the
  * state this Animation instance represents, e.g. running, jumping and so on. The mode specifies whether the animation is
  * looping or not.
  * 
  * @param stateTime the time spent in the state represented by this animation.
  * @param looping whether the animation is looping or not.
  * @return the TextureRegion representing the frame of animation for the given state time. */
 public TextureRegion getKeyFrame (float stateTime, boolean looping) {
  // we set the play mode by overriding the previous mode based on looping
  // parameter value
  if (looping && (playMode == NORMAL || playMode == REVERSED)) {
   if (playMode == NORMAL)
    playMode = LOOP;
   else
    playMode = LOOP_REVERSED;
  } else if (!looping && !(playMode == NORMAL || playMode == REVERSED)) {
   if (playMode == LOOP_REVERSED)
    playMode = REVERSED;
   else
    playMode = LOOP;
  }

  return getKeyFrame(stateTime);
 }

 /** Returns a {@link TextureRegion} based on the so called state time. This is the amount of seconds an object has spent in the
  * state this Animation instance represents, e.g. running, jumping and so on using the mode specified by
  * {@link #setPlayMode(int)} method.
  * 
  * @param stateTime
  * @return the TextureRegion representing the frame of animation for the given state time. */
 public TextureRegion getKeyFrame (float stateTime) {
  int frameNumber = getKeyFrameIndex (stateTime);
  return keyFrames[frameNumber];
 }
 
 /** Returns the current frame number.
  * @param stateTime
  * @return current frame number */
 public int getKeyFrameIndex (float stateTime) {
  int frameNumber = (int)(stateTime / frameDuration);

  if(keyFrames.length == 1)
         return 0;
  
  switch (playMode) {
  case NORMAL:
   frameNumber = Math.min(keyFrames.length - 1, frameNumber);
   break;
  case LOOP:
   frameNumber = frameNumber % keyFrames.length;
   break;
  case LOOP_PINGPONG:
   frameNumber = frameNumber % ((keyFrames.length * 2) - 2);
   //if (frameNumber >= keyFrames.length)
   frameNumber = keyFrames.length -1 - Math.abs(frameNumber - keyFrames.length + 1);//keyFrames.length - 2 - (frameNumber - keyFrames.length);
   break;
  case LOOP_RANDOM:
   frameNumber = MathUtils.random(keyFrames.length - 1);
   break;
  case REVERSED:
   frameNumber = Math.max(keyFrames.length - frameNumber - 1, 0);
   break;
  case LOOP_REVERSED:
   frameNumber = frameNumber % keyFrames.length;
   frameNumber = keyFrames.length - frameNumber - 1;
   break;

  default:
   // play normal otherwise
   frameNumber = Math.min(keyFrames.length - 1, frameNumber);
   break;
  }
  
  return frameNumber;
 }

 /** Sets the animation play mode.
  * 
  * @param playMode can be one of the following: Animation.NORMAL, Animation.REVERSED, Animation.LOOP, Animation.LOOP_REVERSED,
  *           Animation.LOOP_PINGPONG, Animation.LOOP_RANDOM */
 public void setPlayMode (int playMode) {
  this.playMode = playMode;
 }

 /** Whether the animation would be finished if played without looping (PlayMode Animation#NORMAL), given the state time.
  * @param stateTime
  * @return whether the animation is finished. */
 public boolean isAnimationFinished (float stateTime) {
  if(playMode != NORMAL && playMode != REVERSED) return false;
  int frameNumber = (int)(stateTime / frameDuration);
  return keyFrames.length - 1 < frameNumber;
 }
}

All that is different is that on PINGPONG mode, it doesn't linger extra long at the ends.  I had to update all my import commands to import mine, instead of libgdx's.

So there you go, we created an animation system using entities and components.

To  bring you up to speed, my project currently has the following structure
  • GameXYZ
  • EntityFactory
  • Components
    • Expires
    • Player
    • Position
    • Sprite
    • SpriteAnimation
  • Systems
    • ExpiringSystem
    • HudRenderSystem
    • SpriteAnimationSystem
    • SpriteRenderSystem
  • Custom
    • Animation
  • Utils
    • ImagePacker
Then, of course, in the Desktop project I have a Launcher.  I know I'm not using Expiring or Player yet, but they seem pretty important for down the line, and I probably won't change them a bit

You have gained 100 XP.  Progress to Level 3: 100/600

5 comments:

  1. Hey, great article !

    I am very new to the Artemis framework (and new to the design pattern as well)
    First i was very exited about the flexibility it seams to offer.
    But the more i am porting parts of my unfinished gameproject based on an hirachy system i am realizing that it also comes with a lot of compromises.

    This particular spriteanimation problem is a good example.
    On the one hand i think the way you handle it by treating each animation frame by the Sprite class is very clean.

    On the other hand what can be done if you want to create an entity that should have a sprite and an animation ?
    Right now if you have a Sprite and Animation Component it is treated as Animation by the Systems.
    I could think of some ways to fix this but then it won't be so clean anymore.

    Another problem occures if you want to handle multiple animated sprites.
    Right now you have an animated sprite facing south.
    But maybe based on your inputsystem you may want to change to north, east or west depending on the direction you want to move with your entity.

    Well you could change the Sprite.name everytime you are traveling in a different direction.
    But that would increase the dependencies even more.
    If you then want to build an entity that is controllable you have to add an Input, Sprite and AnimatedSprite component.

    I really don't want to bash on your implementation and it is propably a clever way to do things for a particular case.

    Right now i am thinking about to handle Animations in a seperate System that has no dependencies with the sprites but i am still unsure if it is the right approach.














    ReplyDelete
    Replies
    1. Hi, and thanks for your comment! I went through a very similar argument in my head about what I /wanted/ my animation system to look like: in particular I don't like how clunky it would be to change direction, as you mentioned.

      They way I implemented it myself was to remove the animation component, then re-add a new one, pointing in the right direction. Clearly this eats up unnecessary resources and would be a pain in the but to manage.

      I also thought of further modifying the Animation.java class to include a list of separate animations, accessible by some key. For instance, perhaps warrior_0 through warrior_6 represent walking down, warrior_7 through warrior_13 represent left, etc... Which range you loop over could be determined by setting a key: down=0, left=1, etc.

      Then the SpriteAnimation component would hold this key. Then, performing actions (however that may someday be handled) would just change the key value, which automatically changes which range Animation loops over.

      As for your first point, I can't think of any cases where I would want to have BOTH a sprite and an animation separately. I do, however, dislike that I have to have both in my implementation. I could imagine getting rid of the Sprite component altogether, replacing it with an animation with just one frame.

      And don't feel bad about bashing my implementations (not that you were even). I have no illusions that I know what I'm doing... I can only hope that people with a better idea will come and post here about what an idiot I was!

      Thanks again, and I wish you the best of luck porting your gameproject, whether you decide Artemis is worth the effort or not!

      Delete
    2. Hey ! :)
      I have been buisy porting further chunks of my game and i have to say i am kinda getting into this step by step (babysteps !).

      As for the AnimationSystem i have modified your version by just a few lines of code. I still think that rendering the animation by the sprite class is a good thing.

      "For instance, perhaps warrior_0 through warrior_6 represent walking down, warrior_7 through warrior_13 represent left, etc... Which range you loop over could be determined by setting a key: down=0, left=1, etc." Dave

      ...is exactly what i did:

      SpriteAnimationSystem: http://pastebin.com/Uxh2CnLL
      SpriteRenderSystem: http://pastebin.com/1XDbzFMn

      Overall i think this framework is a very cool tool that forces you to implement every system very carefully with as little inter-system communications as possible but then it seems so robust and fun to play with.

      Delete
  2. This is just what I needed! I thought long about this myself (Animated sprites), but couldn't come up with anything suitable.

    Although, theoretically, I am not allowed to use any of your code since you haven't explicitly stated any license or permission.

    Would you be okay if I state something like "derived from a work written by xxx from javagamexyz.blogspot.se" in my source-code? :)

    ReplyDelete
    Replies
    1. I hadn't made things clear by this point, but all the code is licensed under the New BSD (3-clause) License, so you are more than welcome to use the code.

      http://opensource.org/licenses/BSD-3-Clause
      https://code.google.com/p/javagamexyz/

      (I haven't actually spent much time making the license clear in the code - just referencing the URL where you got it is fine by me!)

      Delete