I now have a semi-functional GUI system that uses the ECS style to build gui elements rather than inheritance.

master
Zed A. Shaw 3 weeks ago
parent 615599084a
commit 46de98e6f4
  1. 1
      color.hpp
  2. 64
      combat_ui.cpp
  3. 16
      combat_ui.hpp
  4. 65
      ecs_gui.cpp
  5. 77
      ecs_gui.hpp
  6. 8
      gui.cpp
  7. 75
      gui_gadgets.hpp
  8. 9
      lel.cpp
  9. 10
      lel.hpp
  10. 4
      meson.build
  11. 28
      tests/ecs_gui.cpp
  12. 4
      tests/lel.cpp

@ -1,4 +1,5 @@
#pragma once
#include <SFML/Graphics/Color.hpp>
namespace ColorValue {
const sf::Color BLACK{0, 0, 0};

@ -4,62 +4,30 @@
namespace gui {
CombatUI::CombatUI(GameLevel level) :
$layout(RAY_VIEW_X, RAY_VIEW_HEIGHT,
RAY_VIEW_WIDTH, SCREEN_HEIGHT - RAY_VIEW_HEIGHT),
$level(level),
$font{FONT_FILE_NAME}
$level(level)
{
bool good = $layout.parse($grid);
dbc::check(good, "failed to parse combat layout");
$gui.position(RAY_VIEW_X, RAY_VIEW_HEIGHT, RAY_VIEW_WIDTH, SCREEN_HEIGHT - RAY_VIEW_HEIGHT);
$gui.layout(
"[*%(100,150)button_attack1 | *%(100,150)button_attack2 | *%(100,150)button_attack3 | *%(100,150)button_heal]"
"[ >.%(100,50)label_hp | *%.(200,50)bar_hp | _ ]");
render();
}
void CombatUI::render() {
$background.setPosition({float($layout.grid_x), float($layout.grid_y)});
$background.setSize({float($layout.grid_w), float($layout.grid_h)});
$background.setFillColor({0, 0, 0});
for(auto& [name, cell] : $layout.cells) {
if(name == "bar_hp") {
$meters.try_emplace(name, cell);
} else if(name == "label_hp") {
gui::Label label(cell, "hp:", $font);
$labels.emplace_back(cell, "hp:", $font);
} else if(name.starts_with("button_")) {
$buttons.try_emplace(name, cell, name, $font);
}
}
}
void CombatUI::draw(sf::RenderWindow& window) {
window.draw($background);
auto &player = $level.world->get_the<components::Player>();
auto &combat = $level.world->get<components::Combat>(player.entity);
float hp_now = float(combat.hp) / float(combat.max_hp);
auto& bar_hp = $meters.at("bar_hp");
bar_hp.set_percent(hp_now);
for(auto& [name, button] : $buttons) {
button.draw(window);
}
for(auto& [name, meter] : $meters) {
meter.draw(window);
}
for(auto& label : $labels) {
label.draw(window);
auto& world = $gui.world();
for(auto& [name, cell] : $gui.cells()) {
auto button = $gui.entity(name);
world.set<lel::Cell>(button, cell);
world.set<Rectangle>(button, {});
world.set<Clickable>(button, {100});
world.set<Textual>(button, {name});
}
$gui.init();
}
void CombatUI::click(int x, int y) {
if(auto name = $layout.hit(x, y)) {
if((*name).starts_with("button_")) {
auto& button = $buttons.at(*name);
button.shape.setFillColor({100, 0, 0});
}
}
void CombatUI::draw(sf::RenderWindow& window) {
$gui.render(window);
}
}

@ -2,25 +2,15 @@
#include "panel.hpp"
#include "levelmanager.hpp"
#include <SFML/Graphics/RenderWindow.hpp>
#include <SFML/Graphics/RectangleShape.hpp>
#include <SFML/Graphics/Font.hpp>
#include <SFML/Graphics/Text.hpp>
#include "lel.hpp"
#include "gui_gadgets.hpp"
#include "ecs_gui.hpp"
namespace gui {
class CombatUI {
public:
std::string $grid =
"[*%(100,150)button_attack1 | *%(100,150)button_attack2 | *%(100,150)button_attack3 | *%(100,150)button_heal]"
"[ >.%(100,50)label_hp | *%.(200,50)bar_hp | _ ]";
lel::Parser $layout;
GUECS $gui;
GameLevel $level;
sf::Font $font;
sf::RectangleShape $background;
std::unordered_map<std::string, gui::Button> $buttons;
std::vector<gui::Label> $labels;
std::unordered_map<std::string, gui::Meter> $meters;
CombatUI(GameLevel level);
void render();

@ -0,0 +1,65 @@
#include "ecs_gui.hpp"
#include "constants.hpp"
GUECS::GUECS() {
$font = make_shared<sf::Font>(FONT_FILE_NAME);
}
void GUECS::position(int x, int y, int width, int height) {
$parser.position(x, y, width, height);
}
void GUECS::layout(std::string grid) {
$grid = grid;
$parser.parse($grid);
}
DinkyECS::Entity GUECS::entity(std::string name) {
auto entity = $world.entity();
$world.set<CellName>(entity, {name});
return entity;
}
void GUECS::init() {
$world.query<lel::Cell, Rectangle>([](const auto &, auto& cell, auto& rect) {
rect.init(cell);
});
$world.query<lel::Cell, Textual>([this](const auto &, auto& cell, auto& text) {
text.init(cell, $font);
});
}
void GUECS::render(sf::RenderWindow& window) {
$world.query<lel::Cell, Meter>([&](const auto &ent, const auto& cell, const auto &meter) {
if($world.has<Rectangle>(ent)) {
float level = meter.percent * float(cell.w);
auto& target = $world.get<Rectangle>(ent);
// ZED: this 6 is a border width, make it a thing
target.shape->setSize({std::max(level, 0.0f), float(cell.h - 6)});
}
});
$world.query<Rectangle>([&](const auto &, const auto& rect) {
window.draw(*rect.shape);
});
$world.query<Textual>([&](const auto &, const auto& text) {
window.draw(*text.text);
});
}
void GUECS::mouse(sf::RenderWindow &window) {
if(sf::Mouse::isButtonPressed(sf::Mouse::Button::Left)) {
sf::Vector2f pos = window.mapPixelToCoords(sf::Mouse::getPosition(window));
$world.query<lel::Cell, Clickable>([&](const auto &ent, auto& cell, auto &clicked) {
if((pos.x >= cell.x && pos.x <= cell.x + cell.w) &&
(pos.y >= cell.y && pos.y <= cell.y + cell.h))
{
auto& cn = $world.get<CellName>(ent);
fmt::println("clicked on entity {} with name {} and event {}",
ent, cn.name, clicked.event);
}
});
}
}

@ -0,0 +1,77 @@
#pragma once
#include "color.hpp"
#include "dinkyecs.hpp"
#include "lel.hpp"
#include <string>
#include <memory>
#include <SFML/Graphics.hpp>
using std::shared_ptr, std::make_shared;
struct Textual {
std::string label;
shared_ptr<sf::Font> font = nullptr;
shared_ptr<sf::Text> text = nullptr;
void init(lel::Cell &cell, shared_ptr<sf::Font> font_ptr) {
font = font_ptr;
text = make_shared<sf::Text>(*font, label);
auto bounds = text->getLocalBounds();
auto text_cell = lel::center(bounds.size.x, bounds.size.y, cell);
// this stupid / 2 is because SFML renders from baseline rather than from the claimed bounding box
text->setPosition({float(text_cell.x), float(text_cell.y) - text_cell.h / 2});
}
};
struct Clickable {
int event = 0;
};
struct Rectangle {
shared_ptr<sf::RectangleShape> shape = nullptr;
void init(lel::Cell& cell) {
sf::Vector2f size{(float)cell.w, (float)cell.h};
if(shape == nullptr) shape = make_shared<sf::RectangleShape>(size);
shape->setPosition({float(cell.x + 3), float(cell.y + 3)});
shape->setSize({float(cell.w - 6), float(cell.h - 6)});
shape->setFillColor(ColorValue::DARK_MID);
shape->setOutlineColor(ColorValue::MID);
shape->setOutlineThickness(1);
}
};
struct Meter {
float percent = 100.0;
};
struct CellName {
std::string name;
};
class GUECS {
public:
DinkyECS::World $world;
shared_ptr<sf::Font> $font = nullptr;
lel::Parser $parser;
std::string $grid = "";
GUECS();
void position(int x, int y, int width, int height);
void layout(std::string grid);
DinkyECS::Entity entity(std::string name);
inline lel::CellMap& cells() {
return $parser.cells;
}
inline DinkyECS::World& world() {
return $world;
}
void init();
void render(sf::RenderWindow& window);
void mouse(sf::RenderWindow &window);
};

@ -361,10 +361,8 @@ namespace gui {
}
void FSM::mouse() {
if(sf::Mouse::isButtonPressed(sf::Mouse::Button::Left)) {
sf::Vector2f pos = $window.mapPixelToCoords(sf::Mouse::getPosition($window));
$combat_view.click(pos.x, pos.y);
}
// need to sort out how this will be easier with multiple UIs
$combat_view.$gui.mouse($window);
}
void FSM::generate_map() {
@ -411,6 +409,8 @@ namespace gui {
} else {
$status_view.log("You MISSED the enemy.");
}
// this is where we tell the combat ui about the damage
}
break;
case eGUI::COMBAT_START:

@ -1,75 +0,0 @@
#pragma once
#include <SFML/Graphics/RectangleShape.hpp>
#include <SFML/Graphics/Font.hpp>
#include <SFML/Graphics/Text.hpp>
#include "lel.hpp"
namespace gui {
struct Label {
sf::Font &font;
sf::Text text;
lel::Cell& cell;
Label(lel::Cell& cell, std::string label_text, sf::Font &font) :
font(font),
text(font, label_text),
cell(cell)
{
auto bounds = text.getLocalBounds();
auto text_cell = lel::center(bounds.size.x, bounds.size.y, cell);
// this stupid / 2 is because SFML renders from baseline rather than from the claimed bounding box
text.setPosition({float(text_cell.x), float(text_cell.y) - text_cell.h / 2});
}
void draw(sf::RenderWindow& window) {
window.draw(text);
}
};
struct Button {
Label label;
lel::Cell& cell;
sf::RectangleShape shape;
Button(lel::Cell& cell, std::string label_text, sf::Font &font) :
label(cell, label_text, font),
cell(cell)
{
shape.setPosition({float(cell.x + 3), float(cell.y + 3)});
shape.setSize({float(cell.w - 6), float(cell.h - 6)});
shape.setFillColor(ColorValue::DARK_MID);
shape.setOutlineColor(ColorValue::MID);
shape.setOutlineThickness(1);
}
void draw(sf::RenderWindow& window) {
window.draw(shape);
label.draw(window);
}
};
struct Meter {
sf::RectangleShape shape;
lel::Cell& cell;
Meter(lel::Cell& cell) :
cell(cell)
{
shape.setPosition({float(cell.x + 3), float(cell.y + 3)});
shape.setSize({float(cell.w - 6), float(cell.h - 6)});
shape.setFillColor(ColorValue::DARK_MID);
shape.setOutlineColor(ColorValue::MID);
shape.setOutlineThickness(1);
}
void draw(sf::RenderWindow& window) {
window.draw(shape);
}
void set_percent(float percent) {
float level = percent * float(cell.w);
// ZED: this 6 is a border to make it a thing
shape.setSize({std::max(level, 0.0f), float(cell.h - 6)});
}
};
}

@ -16,6 +16,15 @@ namespace lel {
{
}
Parser::Parser() : cur(0, 0) { }
void Parser::position(int x, int y, int width, int height) {
grid_x = x;
grid_y = y;
grid_w = width;
grid_h = height;
}
void Parser::id(std::string name) {
if(name != "_") {
dbc::check(!cells.contains(name),

@ -6,7 +6,6 @@
#include <vector>
namespace lel {
using Row = std::vector<std::string>;
struct Cell {
int x = 0;
@ -26,8 +25,12 @@ namespace lel {
bool percent = false;
Cell(int col, int row) : col(col), row(row) {}
Cell() {}
};
using Row = std::vector<std::string>;
using CellMap = std::unordered_map<std::string, Cell>;
struct Parser {
int grid_x = 0;
int grid_y = 0;
@ -35,9 +38,12 @@ namespace lel {
int grid_h = 0;
Cell cur;
std::vector<Row> grid;
std::unordered_map<std::string, Cell> cells;
CellMap cells;
Parser(int x, int y, int width, int height);
Parser();
void position(int x, int y, int width, int height);
void id(std::string name);
void reset();
bool parse(std::string input);

@ -8,7 +8,7 @@ project('raycaster', 'cpp',
# use this for common options only for our executables
cpp_args=[]
# these are passed as override_defaults
exe_defaults = ['warning_level=2', 'werror=false']
exe_defaults = ['warning_level=2', 'werror=true']
cc = meson.get_compiler('cpp')
@ -61,6 +61,7 @@ sources = [
'config.cpp',
'dbc.cpp',
'devices.cpp',
'ecs_gui.cpp',
'gui.cpp',
'inventory.cpp',
'lel.cpp',
@ -91,6 +92,7 @@ executable('runtests', sources + [
'tests/base.cpp',
'tests/dbc.cpp',
'tests/dinkyecs.cpp',
'tests/ecs_gui.cpp',
'tests/fsm.cpp',
'tests/inventory.cpp',
'tests/lel.cpp',

@ -0,0 +1,28 @@
#include <catch2/catch_test_macros.hpp>
#include <fmt/core.h>
#include "constants.hpp"
#include "ecs_gui.hpp"
TEST_CASE("prototype one gui", "[ecs-gui]") {
GUECS gui;
gui.position(0, 0, 1000, 500);
gui.layout("[test1|test2|test3][test4|_|test5]");
for(auto& [name, cell] : gui.cells()) {
auto& world = gui.world();
auto button = gui.entity(name);
world.set<lel::Cell>(button, cell);
world.set<Rectangle>(button, {});
world.set<Clickable>(button, {});
world.set<Textual>(button, {name});
}
gui.init();
// at this point it's mostly ready but I'd need to render it to a window real quick
sf::RenderWindow window;
window.setSize({SCREEN_WIDTH, SCREEN_HEIGHT});
gui.render(window);
window.display();
}

@ -2,10 +2,6 @@
#include <catch2/catch_test_macros.hpp>
#include <fmt/core.h>
#include <string>
#include "ansi_parser.hpp"
#include <codecvt>
#include <iostream>
#include <vector>
TEST_CASE("test basic ops", "[lel]") {
lel::Parser parser(0, 0, 500, 500);

Loading…
Cancel
Save