novelrt / NovelRT

A cross-platform 2D game engine accompanied by a strong toolset for visual novels.
MIT License
183 stars 43 forks source link

Rewrite NovelRT::Animation::SpriteAnimator to NovelRT::Animation::ValueAnimator<T> #148

Open RubyNova opened 4 years ago

RubyNova commented 4 years ago

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.

RubyNova commented 4 years ago

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.

FiniteReality commented 4 years ago

Not the button I meant to press.. :unamused:

FiniteReality commented 4 years ago

I've got a work-in-progress screenshot for a new keyframing and animation system, which supports custom easing functions: Screenshot_20200822_214544

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:

From what I can tell the last three points are effectively what exists already, but it requires more polish.