OpenFL - Animation


Animation is the process of changing pictures to create the illusion of movement. We will using it to bring our hero alive, make him walk and breath. It will also be used in our world, moving water, wind blowing but in this case only a camp fire burning.

Coding the logic for sprite animations is complicated, a format for storing the animation data and playing it requires some complicated frame rate logic. Thankfully OpenFL has an open source library Spritesheet, it supplies importers for creating animation data and an AnimatedSprite display object for playing them.

Game object components

First some refactoring, in our world tiles and entities can both be animated and most likely will need some other functionality as well. Currently all component logic belongs to the Entity class which although works, it should be spread throughout all child classes so all can benefit from it. Moving component logic to the base class will mean all game objects have the option of having an of the defined components.

public var input(default, set):InputComponent;
public var move(default, set):MoveComponent;
public var graphics(default, set):GraphicsComponent;

public function update(time:Int) {
    if(input != null){
        input.update(time);
    }

    if(move != null){
        move.update(time);
    }

    if(graphics != null){
        graphics.update(time);
    }
}

Input, move and graphics components are now exposed and the update method will call them each step if defined. Not every game object will make use of every component. You could argue an array of components would be more useful. Add any number of components to a game object and let it run. There are some issues with that approach. First order matters, input must always run first and graphics should always be updated last. How will you gaurentee order? Most importantly components have a public interface, how will you find components to interface with it if they are all in an array? An array of components can work but the biggest benefit right now is keeping this KISS. This will be broken out as needed.

Update loop

 
public function update(time:Int){
    bottomLayer.update(time);
    middleLayer.update(time);
    topLayer.update(time);

    for(i in 0...entities.length){
        entities[i].update(time);
    }
}

The simple update loop we had before is no more. It has been moved into the Map class and expanded to support updating all game objects in our world. First it runs the update for all tiles on each tile. Now not every tile will recieve an update, only tiles which make use of it. Then last all entities in the game are updated.

 
public function onEnterFrame(e:Event) {
    var time = Lib.getTimer();
    var delta = time - _lastTime;

    _game.update(delta);

    _lastTime = time;
}

This is the start of the update loop. Every game has one of these. The OnEnterFrame is called every frame update to move the game one tick. In the case start a movement phase if the proper key is pressed, move the hero a little bit and update the graphics component to properly represent the game state. Before any of this happens a delta has to be computed which is from the last frame tick to the current time. This is used to figure out the amount of time that passed since the last tick. Now of course the frame rate of a game is set to 30 but the computer can of course lag and slow down. Which means it has to keep up. This value is currently only used for the new AnimatedSprite to figure out which frame to show.

Defining animations

The spritesheet library makes it very easy to define animations. It supports many importers for Zoe, Sparrow, Animo and Texture Packer. Here we will be using the basic BitmapImporter. It splits the given bitmapData into a sprite sheet which can then be used in animations.

 
var frameRate:Int = 2;
cloth.spritesheet = BitmapImporter.create(clothBitmapData, columns, yTiles, GameObject.tileSize, tileHeight);

cloth.spritesheet.addBehavior(new BehaviorData("idle_down", [0, 1], true, frameRate));
cloth.spritesheet.addBehavior(new BehaviorData("idle_up", [2, 3], true, frameRate));
cloth.spritesheet.addBehavior(new BehaviorData("idle_right", [4, 5], true, frameRate));
cloth.spritesheet.addBehavior(new BehaviorData("idle_left", [6, 7], true, frameRate));

frameRate = 10;
cloth.spritesheet.addBehavior(new BehaviorData("move_down", [8, 9], true, frameRate));
cloth.spritesheet.addBehavior(new BehaviorData("move_up", [10, 11], true, frameRate));
cloth.spritesheet.addBehavior(new BehaviorData("move_right", [12, 13], true, frameRate));
cloth.spritesheet.addBehavior(new BehaviorData("move_left", [14, 15], true, frameRate));

In the EntityStore class some behaviors are defined for our hero. A behavior is just a single animation sequence, it needs a name and frames to play. The true tells the behavior to loop and the frameRate is used to determine the speed of the animation. The speed of the animation for move is much faster than idle.

Graphics component

In the previous tutorials all the graphics handling was done inside the Entity class because it was quick and dirty. Now it’s time to refactor that out into its own component. This new GraphicsComponent class is responsible for displaying the correct animation based on the gameObjects state and positioning the sprite where the gameObject is located.

 
override public function update(time:Int){
    if(gameObject.orientation == Orientation.LEFT){
        if(gameObject.move.isMoving){
            sprite.showBehavior("move_left", false);
        } else {
            sprite.showBehavior("idle_left", false);
        }
    }

    if(gameObject.orientation == Orientation.DOWN){
        if(gameObject.move.isMoving){
            sprite.showBehavior("move_down", false);
        } else {
            sprite.showBehavior("idle_down", false);
        }
    }

    if(gameObject.orientation == Orientation.UP){
        if(gameObject.move.isMoving){
            sprite.showBehavior("move_up", false);
        } else {
            sprite.showBehavior("idle_up", false);
        }
    }

    if(gameObject.orientation == Orientation.RIGHT){
        if(gameObject.move.isMoving){
            sprite.showBehavior("move_right", false);
        } else {
            sprite.showBehavior("idle_right", false);
        }
    }

    sprite.x = gameObject.x;
    sprite.y = gameObject.y - sprite.height + GameObject.tileSize;
    sprite.update(time);
}

In the update method first thing it does is figure out which animation to show. There is one animation for each direction and state. So if the character decides to move up, the move_up animation will be shown while moving and then the idle_up afterwards. A false is passed in on every animation show to prevent the animation from restarting. Remember the update method is called every frame which means this code will always be restarting the animation before it could show a different frame.

Finally the sprites position is made to match the gameObjects. The last line is the magic where the animated sprite displays the next frame of the animation.

From game logic to game view

You can see in the graphics component it is completely decoupled from the game object. The game object never talks to the graphics component directly, the component has to read the state of the game object every frame and figure out the correct thing to display.This is a great architecture because the game logic should always be decoupled from game view.

So what game logic determines the orientation of a game object? It can be changed in any of the components but never directly inside the game object. All implmentation is kept inside components because if you put logic inside the game object or entity you are essentially saying all game objects will always be like this. Which is almost never true in a game, even if that was true it would make for a very boring game because games are most fun with different mechanics and the best way to create different mechanics is to push that logic inside a component for easy swapping.

 
override public function update(time:Int){
    if(KeyState.isDown(Keyboard.RIGHT) && !gameObject.move.isMoving){
        gameObject.move.moveRight();
        gameObject.orientation = Orientation.RIGHT;
    }

    if(KeyState.isDown(Keyboard.LEFT) && !gameObject.move.isMoving){
        gameObject.move.moveLeft();
        gameObject.orientation = Orientation.LEFT;
    }

    if(KeyState.isDown(Keyboard.DOWN) && !gameObject.move.isMoving){
        gameObject.move.moveDown();
        gameObject.orientation = Orientation.DOWN;
    }

    if(KeyState.isDown(Keyboard.UP) && !gameObject.move.isMoving){
        gameObject.move.moveUp();
        gameObject.orientation = Orientation.UP;
    }
}

This may surprise you but the InputComponent is the one to decide the orientation because the input is basically the user saying “I want to go this way.” The MoveComponent might then go in that direction. It’s also nice to break up game object orientation and movement because who is to say moving down will always make the game object look down. For example an entity can be pushed back which involves a movement but doesn’t change orientation.

Tile animations

Tile animations were tricky. Only some tiles are animated so it doesn’t make sense to add every tile to the update loop. Animated tiles need a special animated sprite to render the animations but regular tiles just need a bitmap to render. Animation data needs to be defined on the same TileData where regular tiles are define. The best solution is to define all tile animations in one spritesheet and render all tiles in an animated sprite even if it isn’t animated. Of course a graphics component can be created to handle both cases but it didn’t seem necessary.

 
spritesheet = BitmapImporter.create(tilesheetData, totalXTiles, totalYTiles, TILE_SIZE, TILE_SIZE);
spritesheet.addBehavior(new BehaviorData("52", [52, 53, 54], true, 5));

In the TileSheet class a spritesheet is created to hold all animations. It can also double as a way of getting frames for non-animated tiles. A new behavior is added to define an animation, the name of the bitmap is used as an easy reference for starting the animation.

 
public function setTile(x:Int, y:Int, value:Int) {
    var tileData:TileData = _tilesheet.getTileData(value);

    var tile = getTile(x, y);
    tile.tileData = tileData;

    if(tile.needsUpdate){
        addToUpdates(tile);
    } else {
        removeFromUpdates(tile);
    }
}

The TileLayer class gets a new array to hold all tiles which need an update every frame. The setTile method will add or remove the tile from the update array depending on if the tile needs it.

 
public var spritesheet:Spritesheet;

public function get_bitmapData():BitmapData {
    return spritesheet.getFrame(id).bitmapData;
}

The TileData class needs a way of holding animation data about the tile. The best way of doing this is to the spritesheet from the Tilesheet class. The issue now is getting individual frames for non-animated tiles. The spritesheet class hold all animation and bitmap sprites but there is no easy way of getting the bitmapData. The solution was to implement a new setter bitmapData to easily retrieve that data.

 
override public function update(time:Int){
    sprite.update(time);
}

public function get_needsUpdate():Bool{
    return tileData.animated;
}

public function set_tileData(tileData:TileData) {
    this.tileData = tileData;

    walkable = tileData.walkable;
    sprite.bitmap.bitmapData = tileData.bitmapData;
    sprite.x = x;
    sprite.y = y;

    if(tileData.animated){
        sprite.spritesheet = tileData.spritesheet;
        sprite.showBehavior(Std.string(tileData.id));
    }

    return tileData;
}

In the Tile class the set tileData method is changed to handle animated tiles. Special care was put in the ensure the tile can be changed any time without its state getting wonky. Right after setting the tileData it will start playing its animation. The Std.string is a special method for converting anything into a string. It also has methods for converting to int.

Thats the end of the tutorial remember to checkout the source.

Related Posts

Test Your Chinese Using This Quiz

Using Sidekiq Iteration and Unique Jobs

Using Radicale with Gnome Calendar

Why I Regret Switching from Jekyll to Middleman for My Blog

Pick Random Item Based on Probability

Quickest Way to Incorporate in Ontario

Creating Chinese Study Decks

Generating Better Random Numbers

Image Magick Tricks

My Game Dev Process