299 lines
8.9 KiB
C++
299 lines
8.9 KiB
C++
#include "animate2.hpp"
|
|
#include <memory>
|
|
#include <chrono>
|
|
#include "dbc.hpp"
|
|
#include "rand.hpp"
|
|
#include <iostream>
|
|
#include <fstream>
|
|
#include "sound.hpp"
|
|
#include "components.hpp"
|
|
|
|
constexpr float SUB_FRAME_SENSITIVITY = 0.999f;
|
|
|
|
namespace animate2 {
|
|
using namespace std::chrono_literals;
|
|
|
|
std::vector<sf::IntRect> Animate2::calc_frames() {
|
|
dbc::check(sequence.frames.size() == sequence.durations.size(), "sequence.frames.size() != sequence.durations.size()");
|
|
|
|
std::vector<sf::IntRect> frames;
|
|
|
|
for(int frame_i : sequence.frames) {
|
|
dbc::check(frame_i < sheet.frames, "frame index greater than sheet frames");
|
|
frames.emplace_back(
|
|
sf::Vector2i{sheet.frame_width * frame_i, 0}, // NOTE: one row only for now
|
|
sf::Vector2i{sheet.frame_width,
|
|
sheet.frame_height});
|
|
}
|
|
|
|
return frames;
|
|
}
|
|
|
|
void Animate2::play() {
|
|
dbc::check(!playing, "can't call play while playing?");
|
|
sequence.current = 0;
|
|
sequence.subframe = 0.0f;
|
|
sequence.loop_count = 0;
|
|
playing = true;
|
|
sequence.timer.start();
|
|
sequence.INVARIANT();
|
|
}
|
|
|
|
void Animate2::stop() {
|
|
playing = false;
|
|
sequence.timer.reset();
|
|
}
|
|
|
|
// need one for each kind of thing to animate
|
|
// NOTE: possibly find a way to only run apply on frame change?
|
|
void Animate2::apply(sf::Sprite& sprite) {
|
|
dbc::check(sequence.current < $frame_rects.size(), "current frame past $frame_rects");
|
|
// NOTE: pos is not updated yet
|
|
auto& rect = $frame_rects.at(sequence.current);
|
|
sprite.setTextureRect(rect);
|
|
}
|
|
|
|
void Animate2::motion(sf::View& view_out, sf::Vector2f pos, sf::Vector2f size) {
|
|
dbc::check(size.x > 1.0f && size.y > 1.0f, "motion size must be above 1.0 since it's not a ratio");
|
|
dbc::check(transform.flipped == false, "transform must be false, has no effect on View");
|
|
|
|
sf::Vector2f scale{transform.min_x, transform.min_y};
|
|
|
|
transform.apply(sequence, pos, scale);
|
|
|
|
view_out.setCenter(pos);
|
|
|
|
if(transform.scaled) {
|
|
view_out.setSize({size.x * scale.x, size.y * scale.y});
|
|
} else {
|
|
view_out.setSize(size);
|
|
}
|
|
}
|
|
|
|
void Animate2::apply_effect(std::shared_ptr<sf::Shader> effect) {
|
|
dbc::check(effect != nullptr, "can't apply null effect");
|
|
effect->setUniform("u_time", sequence.timer.getElapsedTime().asSeconds());
|
|
sf::Vector2f u_resolution{float(sheet.frame_width), float(sheet.frame_height)};
|
|
effect->setUniform("u_resolution", u_resolution);
|
|
}
|
|
|
|
void Animate2::play_sound() {
|
|
// BUG: this can be optimized way better
|
|
if(sounds.contains(form_name)) {
|
|
for(auto& [at_frame, sound_name] : sounds.at(form_name)) {
|
|
if(sequence.current == at_frame) {
|
|
sound::play(sound_name);
|
|
}
|
|
}
|
|
} else {
|
|
fmt::println("Animation has not sound {}", form_name);
|
|
}
|
|
}
|
|
|
|
/* REFACTOR: I believe this is wrong still. If ::commit() determines number of ticks+alpha since last
|
|
* render then update needs to be called 1/tick. The Timer will keep track of alpha as the error
|
|
* between commit calls, so this function only really needs to care about ticks. But, I'm still
|
|
* calling getElapsedTime() when I already did that in commit(), so should I just ignore that and assume
|
|
* elapsed is DELTA, or use elapsed here?
|
|
*/
|
|
void Animate2::update() {
|
|
dbc::check(playing, "attempt to update animation that's not playing");
|
|
sequence.INVARIANT();
|
|
|
|
auto [ticks, alpha] = sequence.timer.commit();
|
|
int duration = sequence.durations.at(sequence.current);
|
|
sequence.subframe += ticks;
|
|
|
|
sequence.easing_position += ticks;
|
|
|
|
bool frame_change = false;
|
|
|
|
if(sequence.subframe >= duration) {
|
|
sequence.timer.restart();
|
|
sequence.current++;
|
|
sequence.subframe = 0;
|
|
frame_change = true;
|
|
}
|
|
|
|
if(sequence.current >= sequence.frame_count) {
|
|
sequence.loop_count++;
|
|
sequence.easing_position = 0;
|
|
playing = onLoop(sequence, transform);
|
|
sequence.INVARIANT();
|
|
}
|
|
|
|
if(frame_change) play_sound();
|
|
|
|
if(frame_change && onFrame != nullptr) onFrame();
|
|
}
|
|
|
|
void Animate2::motion(sf::Transformable& sprite, sf::Vector2f pos, sf::Vector2f scale) {
|
|
sequence.INVARIANT();
|
|
|
|
transform.apply(sequence, pos, scale);
|
|
|
|
if(transform.flipped) {
|
|
scale.x *= -1;
|
|
}
|
|
|
|
sprite.setPosition(pos);
|
|
|
|
if(transform.scaled) {
|
|
sprite.setScale(scale);
|
|
}
|
|
}
|
|
|
|
void Timer::start() {
|
|
clock.start();
|
|
prev_time = clock.getElapsedTime().asSeconds();
|
|
}
|
|
|
|
void Timer::reset() {
|
|
elapsed_ticks = 0;
|
|
clock.reset();
|
|
}
|
|
|
|
void Timer::restart() {
|
|
elapsed_ticks = 0;
|
|
clock.restart();
|
|
prev_time = clock.getElapsedTime().asSeconds();
|
|
}
|
|
|
|
sf::Time Timer::getElapsedTime() {
|
|
return clock.getElapsedTime();
|
|
}
|
|
|
|
std::pair<int, double> Timer::commit() {
|
|
// determine frame duration based on previous time
|
|
current_time = clock.getElapsedTime().asSeconds();
|
|
frame_duration = current_time - prev_time;
|
|
|
|
// update prev_time for the next call
|
|
prev_time = current_time;
|
|
|
|
// update accumulator, retaining previous errors
|
|
accumulator += frame_duration;
|
|
|
|
// find the tick count based on DELTA
|
|
double tick_count = floor(accumulator / DELTA);
|
|
|
|
// reduce accumulator by the number of DELTAS
|
|
accumulator -= tick_count * DELTA;
|
|
// that leaves the remaining errors for next loop
|
|
|
|
elapsed_ticks += tick_count;
|
|
|
|
// alpha is then what we lerp...but WHY?!
|
|
alpha = accumulator / DELTA;
|
|
|
|
// return the number of even DELTA ticks and the alpha
|
|
return {int(tick_count), alpha};
|
|
}
|
|
|
|
void Transform::apply(Sequence& seq, sf::Vector2f& pos_out, sf::Vector2f& scale_out) {
|
|
float tick = easing_func(seq.easing_position / seq.easing_duration);
|
|
|
|
motion_func(*this, pos_out, scale_out, tick, relative);
|
|
}
|
|
|
|
bool Animate2::has_form(const std::string& as_form) {
|
|
return forms.contains(as_form);
|
|
}
|
|
|
|
void Animate2::set_form(const std::string& as_form) {
|
|
dbc::check(forms.contains(as_form),
|
|
fmt::format("form {} does not exist in animation", as_form));
|
|
stop();
|
|
|
|
const auto& [seq_name, tr_name] = forms.at(as_form);
|
|
|
|
dbc::check(sequences.contains(seq_name),
|
|
fmt::format("sequences do NOT have \"{}\" name", seq_name));
|
|
|
|
dbc::check(transforms.contains(tr_name),
|
|
fmt::format("transforms do NOT have \"{}\" name", tr_name));
|
|
|
|
// everything good, do the update
|
|
form_name = as_form;
|
|
sequence_name = seq_name;
|
|
transform_name = tr_name;
|
|
sequence = sequences.at(seq_name);
|
|
transform = transforms.at(tr_name);
|
|
|
|
sequence.frame_count = sequence.frames.size();
|
|
|
|
// BUG: should this be configurable instead?
|
|
for(auto duration : sequence.durations) {
|
|
sequence.easing_duration += float(duration);
|
|
}
|
|
dbc::check(sequence.easing_duration > 0.0, "bad easing duration");
|
|
|
|
$frame_rects = calc_frames();
|
|
transform.easing_func = ease2::get_easing(transform.easing);
|
|
transform.motion_func = ease2::get_motion(transform.motion);
|
|
|
|
sequence.INVARIANT();
|
|
}
|
|
|
|
Animate2 load(const std::string &file, const std::string &anim_name) {
|
|
using nlohmann::json;
|
|
std::ifstream infile(file);
|
|
auto data = json::parse(infile);
|
|
|
|
dbc::check(data.contains(anim_name),
|
|
fmt::format("{} animation config does not have animation {}", file, anim_name));
|
|
|
|
Animate2 anim;
|
|
animate2::from_json(data[anim_name], anim);
|
|
|
|
dbc::check(anim.forms.contains("idle"),
|
|
fmt::format("animation {} must have 'idle' form", anim_name));
|
|
|
|
anim.set_form("idle");
|
|
|
|
return anim;
|
|
}
|
|
|
|
void Sequence::INVARIANT(const std::source_location location) {
|
|
dbc::check(frames.size() == durations.size(),
|
|
fmt::format("frames.size={} doesn't match durations.size={}",
|
|
frames.size(), durations.size()), location);
|
|
|
|
dbc::check(easing_duration > 0.0,
|
|
fmt::format("bad easing duration: {}", easing_duration), location);
|
|
|
|
dbc::check(frame_count == frames.size(),
|
|
fmt::format("frame_count={} doesn't match frames.size={}", frame_count, frames.size()), location);
|
|
|
|
dbc::check(frame_count == durations.size(),
|
|
fmt::format("frame_count={} doesn't match durations.size={}", frame_count, durations.size()), location);
|
|
|
|
dbc::check(current < durations.size(),
|
|
fmt::format("current={} went past end of fame durations.size={}",
|
|
current, durations.size()), location);
|
|
}
|
|
|
|
// BUG: BAAADD REMOVE
|
|
bool has(const std::string& name) {
|
|
using nlohmann::json;
|
|
std::ifstream infile("assets/animate2.json");
|
|
auto data = json::parse(infile);
|
|
return data.contains(name);
|
|
}
|
|
|
|
void configure(DinkyECS::World& world, DinkyECS::Entity entity) {
|
|
auto sprite = world.get_if<components::Sprite>(entity);
|
|
|
|
if(sprite != nullptr && has(sprite->name)) {
|
|
world.set<Animate2>(entity, animate2::load("assets/animate2.json", sprite->name));
|
|
}
|
|
}
|
|
|
|
void animate_entity(DinkyECS::World &world, DinkyECS::Entity entity) {
|
|
auto anim = world.get_if<Animate2>(entity);
|
|
|
|
if(anim != nullptr && !anim->playing) {
|
|
anim->play();
|
|
}
|
|
}
|
|
}
|