Created a nice utility library for doing animations, and used it in the ritual crafting UI.

master
Zed A. Shaw 2 weeks ago
parent 0a40135f5d
commit 1aa6674e42
  1. 2
      Makefile
  2. 103
      animation.cpp
  3. 14
      animation.hpp
  4. 12
      assets/animations.json
  5. 3
      assets/config.json
  6. BIN
      assets/the_ritual_circle.png
  7. 2
      boss_fight_ui.cpp
  8. 58
      components.cpp
  9. 2
      components.hpp
  10. 4
      main.cpp
  11. 3
      meson.build
  12. 76
      ritual_ui.cpp
  13. 53
      tests/animation.cpp
  14. 31
      tests/components.cpp

@ -22,7 +22,7 @@ tracy_build:
meson compile -j 10 -C builddir
test: build
./builddir/runtests
./builddir/runtests "[animation]"
run: build test
powershell "cp ./builddir/zedcaster.exe ."

@ -0,0 +1,103 @@
#include "animation.hpp"
namespace components {
void Animation::play() {
if(!playing) {
current = 0;
subframe = 0.0f;
playing = true;
}
}
float Animation::twitching() {
switch(easing) {
case ease::NONE:
return 0.0;
case ease::SINE:
return ease::sine(float(frames) / subframe * ease_rate);
case ease::OUT_CIRC:
return ease::out_circ(ease::sine(float(frames) / subframe * ease_rate));
case ease::OUT_BOUNCE:
return ease::out_bounce(ease::sine(float(frames) / subframe * ease_rate));
case ease::IN_OUT_BACK:
return ease::in_out_back(ease::sine(float(frames) / subframe * ease_rate));
default:
dbc::sentinel(
fmt::format("Invalid easing {} given to animation",
int(easing)));
}
}
void Animation::step(sf::Vector2f& scale_out, sf::Vector2f& pos_out, sf::IntRect& rect_out) {
if(playing && current < frames) {
float tick = twitching();
scale_out.x = std::lerp(scale_out.x, scale_out.x + scale, tick);
scale_out.y = std::lerp(scale_out.y, scale_out.y + scale, tick);
if(stationary) {
pos_out.y = pos_out.y - (pos_out.y * scale_out.y - pos_out.y);
}
if(!simple) {
rect_out.position.x += current * frame_width;
}
subframe += speed;
current = int(subframe);
} else if(!looped) {
playing = false;
current = frames - 1;
subframe = float(frames - 1);
if(!simple) {
rect_out.position.x += current * frame_width;
}
} else {
playing = false;
current = 0;
subframe = 0.0f;
}
}
}
namespace animation {
using namespace components;
using namespace textures;
static AnimationManager MGR;
static bool initialized = false;
bool apply(Animation& anim, SpriteTexture& target) {
auto size = target.texture->getSize();
anim.frame_width = int(size.x) / (unsigned int)anim.frames;
sf::IntRect rect{{0,0}, {anim.frame_width, int(size.y)}};
sf::Vector2f scale{1.0, 1.0};
sf::Vector2f pos{0, 0};
anim.step(scale, pos, rect);
target.sprite->setTextureRect(rect);
target.sprite->setPosition(pos);
target.sprite->setScale(scale);
return anim.playing;
}
void init() {
if(!initialized) {
Config config("assets/animations.json");
for(auto& [name, data] : config.json().items()) {
auto anim = components::convert<Animation>(data);
MGR.animations.insert_or_assign(name, anim);
}
initialized = true;
}
}
Animation load(std::string name) {
return MGR.animations.at(name);
}
}

@ -0,0 +1,14 @@
#pragma once
#include "components.hpp"
#include "textures.hpp"
#include "easings.hpp"
namespace animation {
struct AnimationManager {
std::unordered_map<std::string, components::Animation> animations;
};
bool apply(components::Animation& anim, textures::SpriteTexture& target);
void init();
components::Animation load(std::string name);
}

@ -0,0 +1,12 @@
{
"ritual_blanket": {
"_type": "Animation",
"easing": 0,
"ease_rate": 0.5,
"scale": 1.0,
"simple": false,
"frames": 3,
"speed": 0.2,
"stationary": true
}
}

@ -52,7 +52,8 @@
"devils_fingers_stage": "assets/devils_fingers_stage.png",
"tunnel_with_rocks": "assets/tunnel_with_rocks.png",
"tunnel_with_rocks_stage": "assets/tunnel_with_rocks_stage.png",
"ritual_crafting_area": "assets/ritual_crafting_area.png"
"ritual_crafting_area": "assets/ritual_crafting_area.png",
"the_ritual_circle": "assets/the_ritual_circle.png"
},
"worldgen": {
"enemy_probability": 50,

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

@ -32,7 +32,7 @@ namespace gui {
void BossFightUI::configure_sprite() {
$sprite_config = $world->get<components::Sprite>($boss_id);
$animation = $world->get<components::Animation>($boss_id);
$animation.texture_width = $sprite_config.width;
$animation.frame_width = $sprite_config.width;
$boss_image = textures::get($sprite_config.name);
sf::IntRect frame_rect{{0,0},{$sprite_config.width,$sprite_config.height}};

@ -28,62 +28,4 @@ namespace components {
components::enroll<Animation>(component_map);
components::enroll<Sound>(component_map);
}
void Animation::play() {
if(!playing) {
current = 0;
subframe = 0.0f;
playing = true;
}
}
float Animation::twitching() {
switch(easing) {
case ease::NONE:
return 0.0;
case ease::SINE:
return ease::sine(float(frames) / subframe * ease_rate);
case ease::OUT_CIRC:
return ease::out_circ(ease::sine(float(frames) / subframe * ease_rate));
case ease::OUT_BOUNCE:
return ease::out_bounce(ease::sine(float(frames) / subframe * ease_rate));
case ease::IN_OUT_BACK:
return ease::in_out_back(ease::sine(float(frames) / subframe * ease_rate));
default:
dbc::sentinel(
fmt::format("Invalid easing {} given to animation",
int(easing)));
}
}
void Animation::step(sf::Vector2f& scale_out, sf::Vector2f& pos_out, sf::IntRect& rect_out) {
if(playing && current < frames) {
float tick = twitching();
scale_out.x = std::lerp(scale_out.x, scale_out.x + scale, tick);
scale_out.y = std::lerp(scale_out.y, scale_out.y + scale, tick);
if(stationary) {
pos_out.y = pos_out.y - (pos_out.y * scale_out.y - pos_out.y);
}
if(!simple) {
rect_out.position.x += current * texture_width;
}
subframe += speed;
current = int(subframe);
} else if(!looped) {
playing = false;
current = frames - 1;
subframe = float(frames - 1);
if(!simple) {
rect_out.position.x += current * texture_width;
}
} else {
playing = false;
current = 0;
subframe = 0.0f;
}
}
}

@ -123,7 +123,7 @@ namespace components {
float subframe = 0.0f;
bool looped = false;
// BUG: this is weirdly not used in most animations but also named wrong should be frame_width
int texture_width = TEXTURE_WIDTH;
int frame_width = TEXTURE_WIDTH;
void play();
float twitching();

@ -3,6 +3,7 @@
#include "sound.hpp"
#include "autowalker.hpp"
#include "ai.hpp"
#include "animation.hpp"
#include <iostream>
int main(int argc, char* argv[]) {
@ -10,6 +11,9 @@ int main(int argc, char* argv[]) {
textures::init();
sound::init();
ai::init("assets/ai.json");
animation::init();
sound::mute(true);
gui::FSM main;
main.event(gui::Event::STARTED);

@ -79,6 +79,8 @@ dependencies += [
sources = [
'ai.cpp',
'ai_debug.cpp',
'animation.cpp',
'animation.cpp',
'autowalker.cpp',
'boss_fight_ui.cpp',
'camera.cpp',
@ -138,6 +140,7 @@ executable('runtests', sources + [
'tests/rituals.cpp',
'tests/sound.cpp',
'tests/spatialmap.cpp',
'tests/animation.cpp',
'tests/stats.cpp',
'tests/textures.cpp',
'tests/tilemap.cpp',

@ -2,6 +2,7 @@
#include "components.hpp"
#include "guecs.hpp"
#include "rand.hpp"
#include "animation.hpp"
namespace gui {
using namespace guecs;
@ -13,12 +14,12 @@ namespace gui {
$gui.position(STATUS_UI_X, STATUS_UI_Y, STATUS_UI_WIDTH, STATUS_UI_HEIGHT);
$gui.layout(
"[_]"
"[inv_slot5 | inv_slot6 | inv_slot7| inv_slot8]"
"[inv_slot9 | inv_slot10 | inv_slot11| inv_slot12]"
"[inv_slot13 | inv_slot14 | inv_slot15| inv_slot16]"
"[inv_slot17 | inv_slot18 | inv_slot19| inv_slot20]"
"[inv_slot21 | inv_slot22 | inv_slot23| inv_slot24]"
"[*%(100,500)circle_area]"
"[*%(100,600)circle_area]"
"[_]"
"[_]"
"[_]"
"[_]"
@ -27,17 +28,17 @@ namespace gui {
}
void RitualUI::init() {
// $gui.world().set_the<Background>({$gui.$parser});
for(auto& [name, cell] : $gui.cells()) {
auto button = $gui.entity(name);
if(name == "circle_area") {
// $gui.set<Rectangle>(button, {});
$gui.set<Sprite>(button, {"the_ritual_circle"});
$gui.set<Clickable>(button, {
[this](auto, auto){ dbc::log("circle clicked"); }
});
} else if(name.starts_with("inv_slot")) {
$gui.set<Sprite>(button, {"the_ritual_circle"});
// $gui.set<Rectangle>(button, {});
$gui.set<Clickable>(button, {
[this, name](auto, auto){ dbc::log(fmt::format("inv_slot {}", name)); }
@ -53,13 +54,7 @@ namespace gui {
$ritual_ui.sprite->setPosition({0,0});
$ritual_ui.sprite->setTextureRect($ritual_closed_rect);
$ritual_state = RitualUIState::CLOSED;
$ritual_anim.simple = false;
$ritual_anim.looped = false;
$ritual_anim.easing = ease::NONE;
$ritual_anim.stationary = true;
$ritual_anim.texture_width = 380;
$ritual_anim.frames = 3;
$ritual_anim.speed = 0.2f;
$ritual_anim = animation::load("ritual_blanket");
$gui.init();
}
@ -75,20 +70,11 @@ namespace gui {
void RitualUI::toggle() {
using enum RitualUIState;
switch($ritual_state) {
case OPEN:
$ritual_state = CLOSING;
break;
case CLOSED:
$ritual_state = OPENING;
$ritual_anim.play();
break;
case OPENING: // ignored
break;
case CLOSING: // ignored
break;
default:
dbc::sentinel("INVALID RitualUIState");
if($ritual_state == OPEN) {
$ritual_state = CLOSING;
} else if($ritual_state == CLOSED) {
$ritual_state = OPENING;
$ritual_anim.play();
}
}
@ -98,40 +84,16 @@ namespace gui {
}
void RitualUI::render(sf::RenderWindow &window) {
sf::IntRect rect;
sf::Vector2f scale{1.0, 1.0};
sf::Vector2f pos{0, 0};
using enum RitualUIState;
switch($ritual_state) {
case OPEN: {
rect = $ritual_open_rect;
} break;
case CLOSED: {
rect = $ritual_closed_rect;
}
break;
case OPENING: {
if($ritual_anim.playing) {
rect = $ritual_closed_rect;
$ritual_anim.step(scale, pos, rect);
} else {
$ritual_state = OPEN;
rect = $ritual_open_rect;
}
}
break;
case CLOSING: {
rect = $ritual_closed_rect;
$ritual_state = CLOSED;
} break;
default:
dbc::sentinel("INVALID RitualUIState");
}
$ritual_ui.sprite->setTextureRect(rect);
$ritual_ui.sprite->setPosition(pos);
$ritual_ui.sprite->setScale(scale);
if($ritual_state == OPENING) {
if(!animation::apply($ritual_anim, $ritual_ui)) {
$ritual_state = OPEN;
}
} else if($ritual_state == CLOSING) {
$ritual_ui.sprite->setTextureRect($ritual_closed_rect);
$ritual_state = CLOSED;
}
window.draw(*$ritual_ui.sprite);
if($ritual_state == OPEN) $gui.render(window);

@ -0,0 +1,53 @@
#include <catch2/catch_test_macros.hpp>
#include "animation.hpp"
#include "dinkyecs.hpp"
#include "config.hpp"
#include <iostream>
using namespace components;
using namespace textures;
TEST_CASE("animation easing tests", "[animation]") {
Animation anim;
anim.easing = ease::NONE;
float res = anim.twitching();
REQUIRE(res == 0.0);
anim.easing = ease::SINE;
anim.subframe = 1.0f;
res = anim.twitching();
REQUIRE(!std::isnan(res));
anim.easing = ease::OUT_CIRC;
res = anim.twitching();
REQUIRE(!std::isnan(res));
anim.easing = ease::OUT_BOUNCE;
res = anim.twitching();
REQUIRE(!std::isnan(res));
anim.easing = ease::IN_OUT_BACK;
res = anim.twitching();
REQUIRE(!std::isnan(res));
anim.easing = ease::FUCKFACE;
bool throws = false;
try { anim.twitching(); } catch(...) { throws = true; }
REQUIRE(throws);
}
TEST_CASE("animation utility API", "[animation]") {
textures::init();
animation::init();
auto blanket = textures::get("ritual_crafting_area");
auto anim = animation::load("ritual_blanket");
anim.play();
while(animation::apply(anim, blanket)) {
fmt::println("animation: {}", anim.subframe);
}
}

@ -60,34 +60,3 @@ TEST_CASE("make sure json_mods works", "[components]") {
auto boss2 = world.get<BossFight>(devils_fingers);
REQUIRE(boss2.stage != std::nullopt);
}
TEST_CASE("animation component special cases", "[components]") {
Animation anim;
anim.easing = ease::NONE;
float res = anim.twitching();
REQUIRE(res == 0.0);
anim.easing = ease::SINE;
anim.subframe = 1.0f;
res = anim.twitching();
REQUIRE(!std::isnan(res));
anim.easing = ease::OUT_CIRC;
res = anim.twitching();
REQUIRE(!std::isnan(res));
anim.easing = ease::OUT_BOUNCE;
res = anim.twitching();
REQUIRE(!std::isnan(res));
anim.easing = ease::IN_OUT_BACK;
res = anim.twitching();
REQUIRE(!std::isnan(res));
anim.easing = ease::FUCKFACE;
bool throws = false;
try { anim.twitching(); } catch(...) { throws = true; }
REQUIRE(throws);
}

Loading…
Cancel
Save