Hi, I'm going to refactor my animation system. That's why I'm here! My game has the following claims to my animation system:
- Common frame animations (clipping rect, origin pos, durations, nothing special)
- Multiple sprite layers: I separated my assets to multiple layers (BodyLegs, BodyTorso, ArmorLegs, ArmorTorso, Helmet, Weapon, Offhand). Each animation (e.g. Attack) has always the same number of frames and always the same duration per frame - nevertheless which layer. This will easly synchronize all layers.
- I also seperated Moving from other Actions to allow some kind of "Run'n'Gun" - aka shoot the enemy while yourself is moving
- Additionally, movement should be looped until it's stopped from outside. Actions should be animated only once.
Previously, I was using Thor's Animation handling, but those multiple layers forced me to use lots of animators for one single entity. Also, because I'm heavily focused on Data-oriented design, I'd like to animate my entities in a tight loop. So (at least in my opinion) Thor's Animation handling isn't what I'm looking for. Also, because I want to the same number of frames and duration across multiple layers, another solution might be more suitable.
Here is what my current approach looks like. The code is just typed in without smashing the compiler on it
So it might be broken. A word about
utils::enum_map: I wrote that thin wrapper around
std::array using some
std::numeric_limits-magic in order to be easily able to organize enum-value-keyed stuff without using
std::(unordered_)map. In fact, its API is working very similar and the example code should be very clear (at least I think
)... So, here's my code:
// ----------------------------------------------------------------------------
// common.hpp
enum class Action {
Idle=0, Use, Attack, Shoot, Cast, Die
};
enum class Layer {
// Responsible for moving
BodyLegs=0, ArmorLegs,
// Responsible for actions
BodyTorso, ArmorTorso, Helmet, Weapon, Offhand
};
struct Frame {
sf::IntRect clipping;
sf::Vector2f origin;
};
// ----------------------------------------------------------------------------
// graphics.hpp
struct RenderComponent {
utils::enum_map<Layer, sf::Sprite> layers;
};
void apply_position(RenderComponent& data, sf::Vector2f const & pos) {
// apply position to all layers
for (auto& pair: data.layers) {
pair.second.setPosition(pos);
}
}
void apply_animation(RenderComponent& data, utils::enum_map<Layer, Frame> const & frames) {
// apply animation to all layers
for (auto const & pair: frames) {
auto& sprite = data.layers[pair.first];
sprite.setTextureRect(pair.second.clipping);
sprite.setOrigin(pair.second.origin);
}
}
void draw_sprite(RenderTarget& target, RenderComponent const & data) {
// draw all layers
for (auto const & pair: data.layers) {
target.draw(pair.second);
}
}
// ----------------------------------------------------------------------------
// animation.hpp
// hold a single frame and a duration for _all_ layers
struct AnimationFrame {
utils::enum_map<Layer, Frame> frames;
sf::Time duration;
};
// holds multiple frames
struct EntireAnimation {
std::vector<AnimationFrame> frames;
};
// holds entire animation configuration: frames per action, frames for movement
struct SpriteAnimation {
utils::enum_map<Action, EntireAnimation> actions;
EntireAnimation move;
};
struct AnimationComponent {
SpriteAnimation const * animation; // non-owning ptr, owned by external system
bool move;
Action action;
std::size_t move_index, action_index; // current frame indices
sf::Time move_time, action_time; // current frame times
};
void update(std::size_t& index, sf::Time& time, EntireAnimation& animation,
bool loop, bool& updated, bool& finished) {
// skip if frame over
finished = false;
updated = false;
while (time >= animation.frames[index].duration) {
time -= animation.frames[index].duration;
++index;
updated = true;
if (index >= animation.frames.size() && ) {
if (!loop) {
// animation finished
index = 0u;
time = sf::Time::Zero;
finished = true;
break;
} else {
// restart animation
index = 0u;
}
}
}
}
void animate(AnimationComponent& AnimationComponent, sf::Time elapsed) {
if (AnimationComponent.animation == nullptr) {
return;
}
auto const & ani = *(AnimationComponent.animation);
bool move_updated, action_updated, finished;
// animate movement if moving
if (AnimationComponent.move) {
AnimationComponent.move_time += elapsed;
update(
AnimationComponent.move_index, AnimationComponent.move_time,
ani.move, false, move_updated, finished
);
}
// animate any action
AnimationComponent.action_time += elapsed;
update(
AnimationComponent.action_index, AnimationComponent.action_time,
ani[AnimationComponent.action], true, action_udpated finished
);
if (finished) {
// stop action
AnimationComponent.action = Action::Idle;
}
if (move_updated || action_updated) {
// apply animation to the corresponding graphics component
}
}
I skipped the "lets-notify-the-graphics-system-about-the-changes"-part because it's out of scope here
Can anyone recommend this or a different approach? Or does someone has an idea how to solve this with Thor? (I'm not completly sure whether Thor can help here or not).
This solution isn't perfect either: I smashed _all_ layers into one
AnimationFrame, so moving is forced to have the same number of frames and duration as e.g. attacking (which is not necessary). But else this example code would be much larger
Greetzs,
Glocke