Merge pull request 'Flappy bird' (#3) from robin_rottstaedt/paradiso:main into main
Reviewed-on: #3 Pull request needs revised naming for the example but seems good to go and inlined. Thanks
This commit is contained in:
commit
f452b40000
13 changed files with 8418 additions and 5 deletions
|
@ -1,2 +1,3 @@
|
||||||
add_subdirectory(simple)
|
add_subdirectory(simple)
|
||||||
add_subdirectory(pong)
|
add_subdirectory(pong)
|
||||||
|
add_subdirectory(flappy_bird)
|
11
examples/flappy_bird/CMakeLists.txt
Normal file
11
examples/flappy_bird/CMakeLists.txt
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
file(GLOB_RECURSE flappy_bird_src "*.cpp")
|
||||||
|
file(GLOB_RECURSE flappy_bird_lib "lib/*.hpp" "lib/*.h")
|
||||||
|
file(GLOB_RECURSE flappy_bird_assets "assets/*")
|
||||||
|
|
||||||
|
add_executable(flappy_bird ${flappy_bird_src} ${flappy_bird_lib})
|
||||||
|
target_link_libraries(flappy_bird paradiso_core)
|
||||||
|
|
||||||
|
add_custom_command(TARGET flappy_bird POST_BUILD
|
||||||
|
COMMAND ${CMAKE_COMMAND} -E copy_directory
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/assets/ $<TARGET_FILE_DIR:flappy_bird>/assets)
|
||||||
|
|
BIN
examples/flappy_bird/assets/background-day.png
Normal file
BIN
examples/flappy_bird/assets/background-day.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 6.9 KiB |
BIN
examples/flappy_bird/assets/base.png
Normal file
BIN
examples/flappy_bird/assets/base.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 470 B |
BIN
examples/flappy_bird/assets/pipe-green.png
Normal file
BIN
examples/flappy_bird/assets/pipe-green.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.5 KiB |
BIN
examples/flappy_bird/assets/yellowbird-downflap.png
Normal file
BIN
examples/flappy_bird/assets/yellowbird-downflap.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 426 B |
BIN
examples/flappy_bird/assets/yellowbird-midflap.png
Normal file
BIN
examples/flappy_bird/assets/yellowbird-midflap.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 425 B |
BIN
examples/flappy_bird/assets/yellowbird-upflap.png
Normal file
BIN
examples/flappy_bird/assets/yellowbird-upflap.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 427 B |
310
examples/flappy_bird/flappy_bird.cpp
Normal file
310
examples/flappy_bird/flappy_bird.cpp
Normal file
|
@ -0,0 +1,310 @@
|
||||||
|
/**
|
||||||
|
* paradiso - Paradigmen der Softwareentwicklung
|
||||||
|
*
|
||||||
|
* (c) Copyright 2023 Hartmut Seichter
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <paradiso/bitmap.hpp>
|
||||||
|
#include <paradiso/context.hpp>
|
||||||
|
#include <paradiso/geometry.hpp>
|
||||||
|
#include <paradiso/renderer.hpp>
|
||||||
|
#include <paradiso/shader.hpp>
|
||||||
|
#include <paradiso/sprite.hpp>
|
||||||
|
#include <paradiso/vector.hpp>
|
||||||
|
#include <paradiso/window.hpp>
|
||||||
|
|
||||||
|
#include <chrono>
|
||||||
|
#include <iomanip>
|
||||||
|
#include <iostream>
|
||||||
|
#include <unordered_map>
|
||||||
|
|
||||||
|
#include "lib/image_loader.hpp"
|
||||||
|
|
||||||
|
const int frame_rate = 60;
|
||||||
|
|
||||||
|
struct Background {
|
||||||
|
paradiso::Sprite backgroundLeft;
|
||||||
|
paradiso::Sprite backgroundRight;
|
||||||
|
paradiso::Sprite grassLeft;
|
||||||
|
paradiso::Sprite grassRight;
|
||||||
|
|
||||||
|
paradiso::Sprite* scrolling[2] = {&backgroundLeft, &backgroundRight};
|
||||||
|
|
||||||
|
Background() {
|
||||||
|
auto backgroundImage =
|
||||||
|
image_loader::load(std::string("background-day.png"));
|
||||||
|
backgroundLeft = paradiso::Sprite{
|
||||||
|
.bitmap = backgroundImage,
|
||||||
|
.pivot = paradiso::Vector2<float>::make(0.0f, 0.0f),
|
||||||
|
.scale = paradiso::Vector2<float>::make(1.01f, 1.0f)};
|
||||||
|
backgroundRight = paradiso::Sprite{
|
||||||
|
.bitmap = backgroundImage,
|
||||||
|
.pivot = paradiso::Vector2<float>::make(2.0f, 0.0f),
|
||||||
|
.scale = paradiso::Vector2<float>::make(1.01f, 1.0f)};
|
||||||
|
|
||||||
|
auto grassImage = image_loader::load(std::string("base.png"));
|
||||||
|
grassLeft = paradiso::Sprite{
|
||||||
|
.bitmap = grassImage,
|
||||||
|
.pivot = paradiso::Vector2<float>::make(0.0f, -1.0f),
|
||||||
|
.scale = paradiso::Vector2<float>::make(1.0f, 0.33333f)};
|
||||||
|
grassRight = paradiso::Sprite{
|
||||||
|
.bitmap = grassImage,
|
||||||
|
.pivot = paradiso::Vector2<float>::make(2.0f, -1.0f),
|
||||||
|
.scale = paradiso::Vector2<float>::make(1.0f, 0.33333f)};
|
||||||
|
}
|
||||||
|
|
||||||
|
void draw(const paradiso::Shader& shader) {
|
||||||
|
for (auto sprite : scrolling) {
|
||||||
|
if (sprite->pivot.x() <= -2.0f) {
|
||||||
|
sprite->pivot.x() += 4.0f;
|
||||||
|
}
|
||||||
|
sprite->pivot.x() -= 0.001f;
|
||||||
|
shader.set_uniform("pivot", sprite->pivot);
|
||||||
|
shader.set_uniform("scale", sprite->scale);
|
||||||
|
shader.set_uniform("rotation", sprite->rotation);
|
||||||
|
renderer.draw(*sprite, shader);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
paradiso::Renderer renderer{};
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Grass {
|
||||||
|
paradiso::Sprite grassLeft;
|
||||||
|
paradiso::Sprite grassRight;
|
||||||
|
|
||||||
|
paradiso::Sprite* scrolling[2] = {&grassLeft, &grassRight};
|
||||||
|
|
||||||
|
Grass() {
|
||||||
|
auto grassImage = image_loader::load(std::string("base.png"));
|
||||||
|
grassLeft = paradiso::Sprite{
|
||||||
|
.bitmap = grassImage,
|
||||||
|
.pivot = paradiso::Vector2<float>::make(0.0f, -0.9f),
|
||||||
|
.scale = paradiso::Vector2<float>::make(1.0f, 0.33333f)};
|
||||||
|
grassRight = paradiso::Sprite{
|
||||||
|
.bitmap = grassImage,
|
||||||
|
.pivot = paradiso::Vector2<float>::make(2.0f, -0.9f),
|
||||||
|
.scale = paradiso::Vector2<float>::make(1.0f, 0.33333f)};
|
||||||
|
}
|
||||||
|
|
||||||
|
void draw(const paradiso::Shader& shader) {
|
||||||
|
for (auto sprite : scrolling) {
|
||||||
|
if (sprite->pivot.x() <= -2.0f) {
|
||||||
|
sprite->pivot.x() += 4.0f;
|
||||||
|
}
|
||||||
|
sprite->pivot.x() -= 0.035f;
|
||||||
|
shader.set_uniform("pivot", sprite->pivot);
|
||||||
|
shader.set_uniform("scale", sprite->scale);
|
||||||
|
shader.set_uniform("rotation", sprite->rotation);
|
||||||
|
renderer.draw(*sprite, shader);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
paradiso::Renderer renderer{};
|
||||||
|
};
|
||||||
|
|
||||||
|
struct FlappyBird {
|
||||||
|
|
||||||
|
paradiso::Renderer renderer1{};
|
||||||
|
paradiso::Renderer renderer2{};
|
||||||
|
paradiso::Renderer renderer3{};
|
||||||
|
|
||||||
|
std::array<paradiso::Sprite, 3> birds;
|
||||||
|
unsigned int flap = 0;
|
||||||
|
unsigned int flapSpeed = 15; // How many ticks per flap
|
||||||
|
unsigned int flapCounter = 0; // How many ticks since last flap
|
||||||
|
|
||||||
|
float velocity = 0.0f;
|
||||||
|
const float max_velocity = 0.05f;
|
||||||
|
|
||||||
|
const float gravity = -0.004f;
|
||||||
|
const float move_up_velocity = 0.0055f;
|
||||||
|
|
||||||
|
bool move_up = false;
|
||||||
|
bool paused = true;
|
||||||
|
|
||||||
|
const float max_pos = 0.95f;
|
||||||
|
const float min_pos = -0.5f;
|
||||||
|
|
||||||
|
float pos = 0.0f;
|
||||||
|
|
||||||
|
float rotation = 0.0f;
|
||||||
|
|
||||||
|
FlappyBird() {
|
||||||
|
float scaleh = 0.07f;
|
||||||
|
float scalew = scaleh * 1.416666666666667f;
|
||||||
|
|
||||||
|
birds = {
|
||||||
|
paradiso::Sprite{
|
||||||
|
.bitmap =
|
||||||
|
image_loader::load(std::string("yellowbird-downflap.png")),
|
||||||
|
.pivot = paradiso::Vector2<float>::make(0.0f, 0.0f),
|
||||||
|
.scale = paradiso::Vector2<float>::make(scalew, scaleh)},
|
||||||
|
paradiso::Sprite{
|
||||||
|
.bitmap =
|
||||||
|
image_loader::load(std::string("yellowbird-midflap.png")),
|
||||||
|
.pivot = paradiso::Vector2<float>::make(0.0f, 0.0f),
|
||||||
|
.scale = paradiso::Vector2<float>::make(scalew, scaleh)},
|
||||||
|
paradiso::Sprite{
|
||||||
|
.bitmap =
|
||||||
|
image_loader::load(std::string("yellowbird-upflap.png")),
|
||||||
|
.pivot = paradiso::Vector2<float>::make(0.0f, 0.0f),
|
||||||
|
.scale = paradiso::Vector2<float>::make(scalew, scaleh)}};
|
||||||
|
}
|
||||||
|
|
||||||
|
void draw(const paradiso::Shader& shader) {
|
||||||
|
// Update flap state
|
||||||
|
if (flapCounter < flapSpeed) {
|
||||||
|
flapCounter++;
|
||||||
|
} else {
|
||||||
|
flapCounter = 0;
|
||||||
|
flap = (flap + 1) % birds.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
auto bird = birds[flap];
|
||||||
|
bird.pivot.y() = pos;
|
||||||
|
bird.rotation = rotation;
|
||||||
|
shader.set_uniform("pivot", bird.pivot);
|
||||||
|
shader.set_uniform("scale", bird.scale);
|
||||||
|
shader.set_uniform("rotation", bird.rotation);
|
||||||
|
|
||||||
|
switch (flap) {
|
||||||
|
case 0:
|
||||||
|
renderer1.draw(bird, shader);
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
renderer2.draw(bird, shader);
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
renderer3.draw(bird, shader);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void update() {
|
||||||
|
// Stop game
|
||||||
|
if (paused)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Apply gravity
|
||||||
|
velocity += gravity;
|
||||||
|
|
||||||
|
if (move_up)
|
||||||
|
velocity += move_up_velocity - gravity;
|
||||||
|
|
||||||
|
// Cap velocity
|
||||||
|
if (velocity > max_velocity)
|
||||||
|
velocity = max_velocity;
|
||||||
|
if (velocity < -max_velocity)
|
||||||
|
velocity = -max_velocity;
|
||||||
|
|
||||||
|
// Cap position
|
||||||
|
pos += velocity;
|
||||||
|
if (pos < min_pos) {
|
||||||
|
pos = min_pos;
|
||||||
|
velocity = 0.0f;
|
||||||
|
}
|
||||||
|
if (pos > max_pos) {
|
||||||
|
pos = max_pos;
|
||||||
|
velocity = 0.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update rotation
|
||||||
|
rotation = velocity * 15.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
// keyboard handler
|
||||||
|
void on_keyboard(const paradiso::Window::KeyboardInputStack& input) {
|
||||||
|
if (input.size()) {
|
||||||
|
paused = false;
|
||||||
|
bool pressed_up = input.top().key == ' ' || input.top().key == 'W';
|
||||||
|
|
||||||
|
if (input.top().action == 1 && pressed_up) {
|
||||||
|
move_up = true;
|
||||||
|
} else if (input.top().action == 0 && pressed_up) {
|
||||||
|
move_up = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
auto main() -> int {
|
||||||
|
|
||||||
|
// Ausgabefenster ... sieht aus als wäre es auf dem Stack
|
||||||
|
auto window = paradiso::Window();
|
||||||
|
|
||||||
|
auto size = paradiso::Size{.width = 500, .height = 700};
|
||||||
|
|
||||||
|
/*
|
||||||
|
window.set_resizecallback([](auto& w) -> void {
|
||||||
|
w.set_size(paradiso::Size{.width = 405, .height = 720});
|
||||||
|
});
|
||||||
|
|
||||||
|
*/
|
||||||
|
window
|
||||||
|
.set_size(size) // ... Grösse
|
||||||
|
.set_position(paradiso::Point{.x = 100, .y = 100}) // ... Position
|
||||||
|
.set_title("PardiSO.FlappyBird") // ... Titel
|
||||||
|
.set_visible(true); // ... und jetzt anzeigen!
|
||||||
|
|
||||||
|
// der Fenster Kontext
|
||||||
|
auto ctx = paradiso::Context{};
|
||||||
|
|
||||||
|
// ein Shader (Schattierungsprogramm)
|
||||||
|
auto shader = paradiso::Shader{};
|
||||||
|
|
||||||
|
// wir nutzen einen vorgefertigten shader
|
||||||
|
shader.load_preset(paradiso::Shader::Preset::Sprite);
|
||||||
|
|
||||||
|
// ein viewport stellt die Sicht der Kamera dar, d.h. bei quadratischen
|
||||||
|
// Pixeln sollte hier auch eine dementsprechende Grösse eingestellt
|
||||||
|
// werden
|
||||||
|
ctx.set_viewport(paradiso::Rectangle{
|
||||||
|
.position = paradiso::Point{.x = 0, .y = 0}, .size = size});
|
||||||
|
|
||||||
|
// nothing beats a classic look
|
||||||
|
ctx.set_clearcolor(paradiso::RGBA::from_rgb(0x00, 0x00, 0x00));
|
||||||
|
|
||||||
|
// Load
|
||||||
|
auto background = Background{};
|
||||||
|
auto grass = Grass{};
|
||||||
|
|
||||||
|
auto flappyBird = FlappyBird{};
|
||||||
|
|
||||||
|
// timer
|
||||||
|
|
||||||
|
// das update führt den hier mitgegebnen Ausdruck innerhalb der internen
|
||||||
|
// Updates des Fensters auf. Es wird hier auch explizit ein bool gefordert
|
||||||
|
// damit das update auch zu jederzeit unterbrochen werden kann
|
||||||
|
while (window.update([&](paradiso::Window& w) -> bool {
|
||||||
|
ctx.set_viewport(paradiso::Rectangle{
|
||||||
|
.position = paradiso::Point{.x = 0, .y = 0}, .size = size});
|
||||||
|
|
||||||
|
ctx.clear();
|
||||||
|
auto t1 = std::chrono::high_resolution_clock::now();
|
||||||
|
|
||||||
|
// Keyboard and state change
|
||||||
|
flappyBird.on_keyboard(w.keyboard_input());
|
||||||
|
flappyBird.update();
|
||||||
|
|
||||||
|
// Draw
|
||||||
|
background.draw(shader);
|
||||||
|
grass.draw(shader);
|
||||||
|
flappyBird.draw(shader);
|
||||||
|
|
||||||
|
// wait for frame rate
|
||||||
|
auto t2 = std::chrono::high_resolution_clock::now();
|
||||||
|
auto duration =
|
||||||
|
std::chrono::duration_cast<std::chrono::microseconds>(t2 - t1);
|
||||||
|
auto wait = std::chrono::microseconds(1000000 / frame_rate) - duration;
|
||||||
|
std::this_thread::sleep_for(wait);
|
||||||
|
|
||||||
|
// Quit
|
||||||
|
return !(w.keyboard_input().size() &&
|
||||||
|
w.keyboard_input().top().key == 'Q');
|
||||||
|
})) {
|
||||||
|
};
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
72
examples/flappy_bird/image_loader.cpp
Normal file
72
examples/flappy_bird/image_loader.cpp
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
#include <paradiso/rgba.hpp>
|
||||||
|
#include <paradiso/sprite.hpp>
|
||||||
|
|
||||||
|
#include <fstream>
|
||||||
|
#include <iostream>
|
||||||
|
#include <optional>
|
||||||
|
#include <unordered_map>
|
||||||
|
|
||||||
|
// STB image loading
|
||||||
|
#define STB_IMAGE_IMPLEMENTATION
|
||||||
|
#include "lib/stb_image.h"
|
||||||
|
|
||||||
|
#include "lib/image_loader.hpp"
|
||||||
|
|
||||||
|
std::unordered_map<std::string, paradiso::Bitmap> image_loader::image_cache =
|
||||||
|
std::unordered_map<std::string, paradiso::Bitmap>();
|
||||||
|
|
||||||
|
paradiso::Bitmap image_loader::load(const std::string& filename) {
|
||||||
|
if (image_cache.find(filename) == image_cache.end()) {
|
||||||
|
std::string path = ASSET_PATH + filename;
|
||||||
|
std::cout << "Loading " << path << std::endl;
|
||||||
|
auto data = readBMP(path);
|
||||||
|
image_cache[filename] = data;
|
||||||
|
}
|
||||||
|
return image_cache[filename];
|
||||||
|
}
|
||||||
|
|
||||||
|
paradiso::Bitmap image_loader::readBMP(std::string& filename) {
|
||||||
|
// Load with stb_image
|
||||||
|
stbi_set_flip_vertically_on_load(true); // flip y axis for OpenGL
|
||||||
|
int width, height, channels;
|
||||||
|
unsigned char* image =
|
||||||
|
stbi_load(filename.c_str(), &width, &height, &channels, 4);
|
||||||
|
int size = width * height;
|
||||||
|
|
||||||
|
if (!image)
|
||||||
|
throw std::runtime_error("Error loading image");
|
||||||
|
|
||||||
|
// Convert to Vector of RGBA
|
||||||
|
std::vector<paradiso::RGBA> rgba = std::vector<paradiso::RGBA>(size);
|
||||||
|
for (int i = 0; i < size; i++) {
|
||||||
|
// get rgba values
|
||||||
|
int pos = i * 4;
|
||||||
|
int r = image[pos + 0];
|
||||||
|
int g = image[pos + 1];
|
||||||
|
int b = image[pos + 2];
|
||||||
|
int a = image[pos + 3];
|
||||||
|
|
||||||
|
// bug in from_rgba. it expects bgra, not rgba
|
||||||
|
auto val = paradiso::RGBA::from_rgba(b, g, r, a);
|
||||||
|
rgba[i] = val;
|
||||||
|
}
|
||||||
|
|
||||||
|
// rotate image by 90 degrees CW
|
||||||
|
std::vector<paradiso::RGBA> rotated = std::vector<paradiso::RGBA>(size);
|
||||||
|
for (int x = 0; x < width; x++) {
|
||||||
|
for (int y = 0; y < height; y++) {
|
||||||
|
|
||||||
|
int pos = y * width + x;
|
||||||
|
int pos_rotated = x * height + (height - y - 1);
|
||||||
|
rotated[pos_rotated] = rgba[pos];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// free image
|
||||||
|
stbi_image_free(image);
|
||||||
|
|
||||||
|
return paradiso::Bitmap::from_data(
|
||||||
|
paradiso::Size{static_cast<unsigned int>(height),
|
||||||
|
static_cast<unsigned int>(width)},
|
||||||
|
rotated);
|
||||||
|
}
|
24
examples/flappy_bird/lib/image_loader.hpp
Normal file
24
examples/flappy_bird/lib/image_loader.hpp
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <paradiso/bitmap.hpp>
|
||||||
|
|
||||||
|
#include <optional>
|
||||||
|
#include <string>
|
||||||
|
#include <unordered_map>
|
||||||
|
|
||||||
|
class image_loader {
|
||||||
|
public:
|
||||||
|
/**
|
||||||
|
* @brief loads an Image from a file
|
||||||
|
* @param[in] filename path to file
|
||||||
|
* @return bitmap from file
|
||||||
|
*/
|
||||||
|
static paradiso::Bitmap load(const std::string& filename);
|
||||||
|
|
||||||
|
private:
|
||||||
|
static inline const std::string ASSET_PATH = "assets/";
|
||||||
|
|
||||||
|
static std::unordered_map<std::string, paradiso::Bitmap> image_cache;
|
||||||
|
|
||||||
|
static paradiso::Bitmap readBMP(std::string& filename);
|
||||||
|
};
|
7987
examples/flappy_bird/lib/stb_image.h
Normal file
7987
examples/flappy_bird/lib/stb_image.h
Normal file
File diff suppressed because it is too large
Load diff
|
@ -54,6 +54,17 @@ struct Bitmap final {
|
||||||
return {.size = size, .data = {values...}};
|
return {.size = size, .data = {values...}};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief creates a bitmap from RGBA Values
|
||||||
|
* @param[in] size extent of bitmap
|
||||||
|
* @param[in] data vector of RGBA values
|
||||||
|
* @return bitmap with data from this call
|
||||||
|
*/
|
||||||
|
static constexpr Bitmap from_data(Size size, std::vector<RGBA> data) noexcept {
|
||||||
|
assert(data.size() == size.height * size.width);
|
||||||
|
return {.size = size, .data = data};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief fills a bitmap uniformly
|
* @brief fills a bitmap uniformly
|
||||||
* @param[in] color RGBA value of a color
|
* @param[in] color RGBA value of a color
|
||||||
|
@ -87,10 +98,7 @@ struct Bitmap final {
|
||||||
return data[y * size.width + x];
|
return data[y * size.width + x];
|
||||||
}
|
}
|
||||||
|
|
||||||
constexpr void force_change() noexcept
|
constexpr void force_change() noexcept { change_count++; }
|
||||||
{
|
|
||||||
change_count++;
|
|
||||||
}
|
|
||||||
|
|
||||||
Size size{.width = 0, .height = 0}; //!< extent of bitmap
|
Size size{.width = 0, .height = 0}; //!< extent of bitmap
|
||||||
std::vector<RGBA> data{}; //!< data storage
|
std::vector<RGBA> data{}; //!< data storage
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue