0 purchases
boomflame
Boomflame #
Providing extendable animations for Flame game engine.
Boomflame
Getting Started
Playing Animations
Changing Animations
Complex Animations (Nodes)
Special Animation Events
On Specific Frames
Attributes
Animation State Attributes
Frame State Attributes
Frame Perfect Games
Terms
Getting Started #
Boomsheets is an animation document format intended to be easily
read or written by anyone without technical knowledge or proprietary tooling.
Unlike other formats, it's intended to be open source and extended.
Note
This means anyone can write an animation document with a plain text editor
or generate them from their own tools.
The underlining spec enables extension with the help of meta-elements
called Attributes. These elements begin with the @ symbol
and can be used to trigger custom behavior without corrupting the playback and
without making any code changes.
Tip
To learn more about Boomsheets documents and format,
visit the repo.
Playing Animations #
Getting started is easy. AnimationComponent expects a SpriteComponent
and the animation document to load and parse. This component will be a child
of the parent SpriteComponent and modify its parent when keyframes change.
// NOTE: This import will be used for all following examples.
// Consider importing with an alias if you are using many different
// AnimationComponent class names in your project to avoid conflicts.
import 'package:boomflame/boomflame.dart';
late SpriteComponent player;
late AnimationComponent playerAnim;
add(player = SpriteComponent(
sprite: await Sprite.load('player.png'),
children: [
playerAnim = AnimationComponent.framebased(
'player.anim',
state: 'dancing',
)
],
));
copied to clipboard
And that's it!
To understand why this is so simple, the constructor is doing a lot for us.
AnimationComponent(
this.src, {
String? state,
String? prefix,
AssetsCache? cache,
this.stateNameSensitivity = CaseSensitivity.insensitive,
this.mode = Mode.forward,
})
copied to clipboard
state is the first animation in the document that you want to see on screen.
By default, all animation documents are stored under /assets/anims/ but can
be changed directly by changing AnimationComponent.prefix and providing a
different asset manager in the constructor.
A mode tells the component how to play the animation and can be changed at
any time.
Tip
Modes can be combined using bitwise operators.
For example Mode.reverse | Mode.loop will loop the animation
while playing it in reverse.
Changing Animations #
Changing animations is also simple.
if(isRunning) {
playerAnim.setState('run', refresh: true);
}
copied to clipboard
Note the behavior of refresh.
If set to true, it will recalculate currKeyframe data,
apply itself to player sprite, and forces the SpriteComponent to
update its bounds and orientation before drawing.
Warning
By default refresh is false out of caution.
Your game logic may change state multiple times before the draw call.
Only set refresh to true if you expect the next frame to be visible
by the time it is displayed on screen in the next draw.
/// On success, sets the [currAnim] animation state.
/// Optionally set [frame] to jump to a keyframe in that state.
/// Default [mode] is [Mode.forward] and overwrites the previous value.
/// If [refresh] param is true, [AnimationComponent.refresh] will run after.
void setState(String state, {int? frame, Mode? mode, bool refresh = false});
copied to clipboard
Tip
If you need further control when to recalculate the frame,
an explicit call to refresh() can be invoked.
Complex Animations (Nodes) #
Some sprites are split into multiple pieces to reduce texture complexity.
Some sprites are complicated and have multiple moving parts.
Some games may need to attach sprites in order to show the player which weapon
is equiped or what gear they are wearing.
Whatever your case may be, Boomsheets has a special element point which
tells each keyframe element the (x,y) pair it is from the origin and its name.
Using point data is easy. Let's see how we might attach a sword to our
hero's base sprite, right onto their hand coordinate:
final String rarity = 'common';
// Add a new sword sprite to the player sprite
await player.add(sword = SpriteComponent(
sprite: await Sprite.load('swords_atlas.png'),
children: [
// Our sword atlas has many sword variations.
// We want only the matching rarity state.
swordsAnim = AnimationComponent(
'swords_atlas.anim',
state: '$rarity_sword',
)
],
));
// Stay in sync with parent AnimationComponent at the
// point named 'hand'.
playerAnim.syncPoint('hand', swordsAnim);
copied to clipboard
It's really that simple. swordsAnim is now in a list of children
of playerAnim. It will synchronize its time to the parent component's
amd update the anchor as well, relative to its parent's latest origin.
Note
Every AnimationComponent can only have one parent at a time.
Special Animation Events #
You may want to trigger functions when some frames are freshly
displayed on screen for the first time, or has finished animating.
For example, you may want to make an explosion play a sound on the first frame
and delete itself from the game world when it completes.
// class Explosion
@override
void update(double dt) {
super.update(dt);
// Remember to always check if you have a valid state and document!
if(anim.currKeyframe == null) return;
if(anim.currKeyframe!.newThisFrame) {
FlameAudio.play("kaboom.wav");
}
}
copied to clipboard
newThisFrame gaurantees that it will only be true when the keyframe data is
retrieved and stored into currKeyframe. Any subsequent call to update(dt)
will set this flag to false, event if the mode is set to Mode.stop.
Now let's remove the explosion after the animation has fully completed:
// class Explosion
@override
void update(double dt) {
super.update(dt);
//
// same code here as before
//
if(anim.currKeyframe!.endedThisFrame) {
removeFromParent();
}
}
copied to clipboard
Again with endedThisFrame we have the same gaurantees as before. Effortless!
On Specific Frames
Some animations impact the user's experience on specific keyframes.
For example, you may have a player animation with their foot making contact
on the fourth and eighth keyframe in the Run state. At that moment, you want
to play a footstep sound to increase immersion, but only the first time the
foot has made contact with the ground! Otherwise the sound will repeat every
call to update(dt)! This is trivial with AnimationComponent.
if (stateIsRunning) {
playerAnim.mode = Mode.loop;
final keyframeIndex = playerAnim.currKeyframe!.index;
final newThisFrame = playerAnim.currKeyframe!.newThisFrame;
if (newThisFrame) {
switch (keyframeIndex) {
case 4 || 8:
FlameAudio.play("step.wav");
}
}
}
copied to clipboard
Attributes #
Your animation documents can contain meta-elements that have additional data.
Per the underlining spec, these elements must come before the
elements they affect and can be stacked in a row.
This implies you can have many attributes and even multiples of the same name!
Tip
This is useful for many reasons. Consider if your game has custom behavior
that should happen when a frame is first displayed on the screen.
Animation State Attributes
Consider a custom level editor for your game. You may want to expose all the
animations that a player can choose from for one of the entities but you don't
want to expose animations that could break the level or should only be present
under special conditions, like a power-up. Here's how our player_sheet.anim
document might look like:
# Note that we're omitting frame data in this document to be brief
@expose
anim super_jumpman_small_run
frame ...
frame ...
@expose
anim super_jumpman_big_run
frame ...
frame ...
anim super_jumpman_die
frame ...
frame ...
frame ...
frame ...
copied to clipboard
We can name our attributes anything. In this example, we have decided to expose
only the animations with @expose in to the editor. Here's how to code that:
game.uiLayer.states.set(
playerAnim
.doc?
.states
.values
.toList()
.where(
(state)=>state.attrs.firstWithName('expose') != null
)
);
copied to clipboard
firstWithName is a special extension for List<Attribute> types only.
It returns Attribute? and will be null if the state element did not
have an attribute with that name.
Frame State Attributes
Attributes can also have their own sets of keys and arguments.
In the previous example, our attribute only had a name and nothing else.
This is fine for simple use cases, but in larger projects, it is essential
to provide meaningful values such as booleans, numbers, or strings.
Note
Internally, all parsed values for keys are strings.
Use getKeyValue, getKeyValueAsInt, getKeyValueAsDouble,
or getKeyValueAsBool to convert them to the proper type.
Let's see how we can use attributes to create new behavior for our frames
that we can inspect and act on in our game!
# fighters/blackbelt_yuki.anim
anim charge_punch
@play_sound charge.wav volume=0.5
frame ...
frame ...
frame ...
@play_sound charge_complete.wav volume=0.5
@play_sound "karate yell.wav"
frame ...
copied to clipboard
In this example, we have a charging hard punch animation that plays a sound
when the animation begins and plays a chime when the charge is complete.
Tip
We can read these attributes in our code to do what we want. And if
we decide we should change the sound, volume, or even the frames they occur
on, we can open the text file and change the properties there without fuss.
Here's how adjustable volume could be implemented in your own game:
// class BaseFighter
@override
void update(double dt) {
super.update(dt);
if(anim?.currKeyframe == null) return;
final attrs = anim!.currKeyframe!.data.attrs;
final sfxList = attrs.allWithName('play_sound');
for(final sfx in sfxList) {
if(sfx.args.isEmpty) continue;
final path = sfx.args[0];
final volume = sfx.getKeyValueAsDouble('volume', 1.0);
FlameAudio.play(path, volume: volume);
}
}
copied to clipboard
Note
The extension allWithName for List<Attribute> is provided to return every
attribute with the same name.
In our competitive fighter game example above,
we may have multiple sound effects playing on the same frame, so we support
this by iterating over all attributes play_sound instead of just one.
Attributes can have any number of arguments; called key-values or KeyVals.
Named KeyVals can be written in any order, but when we mix them with nameless
keys, we need to be careful and ensure all necessary values are present.
Important
In the previous example, notice that the first argument to play_sound
attribute does not have a key. This is a "nameless" keyvalue.
Like named keyvals, we can have any number of them, but we must fetch
such keys by index and convert the value ourselves.
All of this utility comes at no cost. Regardless of what meta-data may be
present in your animations, they will always animate the same way.
Tip
We can take this fighting game example further by supporting attributes
such as hitbox information. Consider an attribute like the following:
@hitbox name, x, y, w, h which we can stack on one keyframe in order
to represent all of the hit zones possible. Very powerful!
Frame Perfect Games #
While the underlining specification of the Boomsheets document uses frames,
they can be used with elapsed seconds which Flame uses in update(dt).
Internally, AnimationComponent keeps both types of clocks: Frametime and
Duration. The former is syntatical sugar over an int type, while the latter
is what is used to convert double dt in update(dt) while preserving as much
precision as possible. But since double is a floating-point primitive,
it can be victim to drift and cause inaccuracies. For most people, this is a
non-issue and can be ignored. But for some, every frame counts.
If you're working with frame-perfect animations, there exists a special
constructor for you: AnimationComponent.framebased(...) as seen in the first
example. Internally, these components ignore dt and instead use tick().
This means you must make sure your game's main loop is also limited to the
framerate you want, otherwise it will animate too quickly and at different
speeds on other people's devices!
Terms #
There are some new terms and ideas here to get adjusted to.
They are provided here to help.
Term
Explanation
Document
A file or string buffer representing a collection of animation states and their keyframe data. Usually ends with .anim suffix.
Anim
A class in Dart representing a collection of keyframe data and attributes.
State
A named collection of keyframes in the animation document. Sometimes it is more convenient to describe the currently playing animation as a "state" to distinguish from animation "files". In the library, Anim class is the state plus other properties such as totalDuration which is used during playback.
Keyframe
A specific frame entry that begins a new subregion from the source texture atlas over a duration of time.
frame
A measurement of time. A frame advances by one every tick(). This term is not related to Keyframe which represents subregion data in an animation state.
tick
A singular update in Flame with the absence of delta time. Delta time or dt for short is useful for simulations, physics-based algorithms, and network games to blend animations together to hide jitter or lag. Delta time causes trouble for frame-sensitive applications such as deterministic online fighter games, retro games, and network games. Yes, delta time can be used to blend network game visuals to smooth latency, but network game state should rely on integer frames in order to gaurantee synchronization!
Attributes
Meta-data. Custom elements that can add new behavior to your animations and tools.
Point
A named (x,y) coordinate relative to the keyframe it is nested under.
Origin
The center of the sprite in a keyframe. This is synonymous with Flame component's anchor field.
For personal and professional use. You cannot resell or redistribute these repositories in their original state.
There are no reviews.