NovelRT
NovelRT copied to clipboard
Rewrite NovelRT::Animation::SpriteAnimator to NovelRT::Animation::ValueAnimator<T>
At this current moment in time, we have NovelRT::Animation::SpriteAnimator
which is hard-coded to update an ImageRect
in a certain fashion based on the engine delta, like so:
void SpriteAnimator::constructAnimation(double delta) {
switch (_animatorState) {
case AnimatorPlayState::Playing: {
if (_currentState == nullptr) {
_currentState = _states.at(0);
_currentState->getFrames()->at(_currentFrameIndex).FrameEnter();
}
auto transitionPtr = _currentState->tryFindValidTransition();
if (transitionPtr != nullptr) {
_currentState = transitionPtr;
_accumulatedDelta = 0.0f;
_currentFrameIndex = 0;
_currentState->getFrames()->at(_currentFrameIndex).FrameEnter();
}
if (_currentState->getFrames()->size() > _currentFrameIndex && _currentState->getFrames()->at(_currentFrameIndex).getDuration() <= _accumulatedDelta) {
_accumulatedDelta = 0;
_currentState->getFrames()->at(_currentFrameIndex++).FrameExit();
if (_currentState->getShouldLoop() && _currentFrameIndex >= _currentState->getFrames()->size()) {
_currentFrameIndex = 0;
} else if (_currentFrameIndex >= _currentState->getFrames()->size()) {
return;
}
auto newFrame = _currentState->getFrames()->at(_currentFrameIndex);
newFrame.FrameEnter();
_rect->setTexture(newFrame.getTexture());
}
_accumulatedDelta += delta;
break;
}
}
}
The proposed rewrite would allow for a lot of this logic to be defined by T
, which we would assert inherits ValueAnimatorDefinition
. This new abstract type would (roughly) look something like this:
class ValueAnimatorDefinition {
public:
void validateInitialState() = 0;
void runAnimationStep(float delta, int32_t frameIndex) = 0;
}
Then a rough implementation for sprites of those two methods would probably look like:
void validateInitialState() {
if (_currentState == nullptr) {
_currentState = _states.at(0);
_currentState->getFrames()->at(_currentFrameIndex).FrameEnter();
}
void runAnimationStep(float delta, int32_t frameIndex) {
if (_currentState->getFrames()->size() > _currentFrameIndex && _currentState->getFrames()->at(_currentFrameIndex).getDuration() <= _accumulatedDelta) {
_accumulatedDelta = 0;
_currentState->getFrames()->at(_currentFrameIndex++).FrameExit();
if (_currentState->getShouldLoop() && _currentFrameIndex >= _currentState->getFrames()->size()) {
_currentFrameIndex = 0;
} else if (_currentFrameIndex >= _currentState->getFrames()->size()) {
return;
}
auto newFrame = _currentState->getFrames()->at(_currentFrameIndex);
newFrame.FrameEnter();
_rect->setTexture(newFrame.getTexture());
}
Then the animator would create an instance of T
and use it to figure out how to manage the animation state machine.
A similar implementation might exist for specific value tweens such as Transform._position
that generates the std::vector<ValueAnimatorFrame>
based on the duration of the ValueAnimatorState
, it may just ignore it completely and use delta
to achieve a similar effect to a basic tweener that I am introducing (soon tm).
If we did do this, at the very least, it would make the actual animator testable, which would be great for code coverage.
I personally think this is a "nice to have" and probably won't happen for quite some time, but as another "thought I had while on the train home", I figured it would be good to share my thoughts.
This probably needs a bit of additional design work to make it what I envisioned fully? And I imagine there will also be questions, but this would be a good feature ticket for @FiniteReality as a nice break from CMake stuff for a while.
Not the button I meant to press.. :unamused:
I've got a work-in-progress screenshot for a new keyframing and animation system, which supports custom easing functions:
That was created byrunning gnuplot with the output of this:
class myThing
{
private:
float _position = 0;
public:
inline const float& position() const { return _position; }
inline float& position() { return _position; }
};
int main()
{
myThing thing;
auto sequence = create_keyframe_sequence(
create_keyframe<ease_out_quad>(0, 10, thing, &myThing::position, 10.0f),
create_keyframe<ease_in_quad>(5, 10, thing, &myThing::position, 20.0f),
create_keyframe<ease_linear>(0, 10, thing, &myThing::position, 30.0f),
create_keyframe<ease_in_out_elastic>(0, 10, thing, &myThing::position, 0.0f)
);
std::ofstream stream("sequence.dat");
stream << sequence.elapsed() << ' ' << thing.position() << '\n';
while (sequence.step(1.0f / 60.0f))
{
stream << sequence.elapsed() << ' ' << thing.position() << '\n';
}
stream << sequence.elapsed() << ' ' << thing.position() << '\n';
}
So the current design I'm thinking of works something like this:
- A keyframe is the smallest unit in the animation system. It correlates time, an easing function, a property and its target value.
- A keyframe sequence is the smallest consumable part of the animation system. It basically manages all of the state necessary for animating from the first keyframe to the last keyframe.
- A state is an object which consists of a keyframe sequence and a list of transitions
- A transition is a predicate and a target state - when the predicate returns truthy, the target state becomes the new state
- A state machine is a wrapper around a state, storing the current state and checking the transitions whether to update or not.
From what I can tell the last three points are effectively what exists already, but it requires more polish.