paradiso/examples/tappyplane/tappyplane.cpp
2025-05-30 19:57:30 +02:00

629 lines
21 KiB
C++

/**
* paradiso - Paradigmen der Softwareentwicklung
*
* (c) Copyright 2023-2025 Hartmut Seichter and Contributors
*
*/
#include <paradiso/bitmap.hpp>
#include <paradiso/bitmap_io.hpp>
#include <paradiso/context.hpp>
#include <paradiso/geometry.hpp>
#include <paradiso/renderer.hpp>
#include <paradiso/shader.hpp>
#include <paradiso/sprite.hpp>
#include <paradiso/utils.hpp>
#include <paradiso/vector.hpp>
#include <paradiso/window.hpp>
#include <chrono>
#include <thread>
#include <iostream>
static const unsigned int FRAME_RATE = 60;
static const unsigned int WINDOW_WIDTH = 800;
static const unsigned int WINDOW_HEIGHT = 480;
static const unsigned int WINDOW_SCALE = 2;
static const char ACTION_KEY = ' ';
// x,y+h --- x+w,y+h
// | |
// | |
// | |
// x,y ----- x+w,y
struct AABB {
paradiso::Vector2<float> position{};
paradiso::Vector2<float> size{};
bool is_colliding_with(const AABB& other) {
return position.x() < other.position.x() + other.size.x() &&
position.x() + size.x() > other.position.x() &&
position.y() < other.position.y() + other.size.y() &&
position.y() + size.y() > other.position.y();
}
};
struct Background {
static constexpr float SCROLLING_SPEED = 0.003f;
std::array<paradiso::Sprite, 2> sprites;
paradiso::Renderer renderer{};
paradiso::Bitmap image =
paradiso::BitmapIO::get().load("PNG/background.png");
void init() {
sprites = {paradiso::Sprite{
.bitmap = image,
.pivot = {paradiso::Vector2<float>::make(0.0f, 0.0f)},
.scale = {paradiso::Vector2<float>::make(1.0f, 1.0f)}},
paradiso::Sprite{
.bitmap = image,
.pivot = {paradiso::Vector2<float>::make(2.0f, 0.0f)},
.scale = {paradiso::Vector2<float>::make(1.0f, 1.0f)}}};
}
void update() {
for (auto& sprite : sprites) {
sprite.pivot.x() -= SCROLLING_SPEED;
if (sprite.pivot.x() <= -2.0f) {
sprite.pivot.x() += 4.0f;
}
}
}
void draw(const paradiso::Shader& shader) {
for (const auto& sprite : sprites) {
shader.set_uniform("pivot", sprite.pivot);
shader.set_uniform("scale", sprite.scale);
shader.set_uniform("rotation", sprite.rotation);
renderer.draw(sprite, shader);
}
}
};
struct Ground {
static constexpr float SCROLLING_SPEED = 0.009f;
std::array<paradiso::Sprite, 2> sprites;
paradiso::Renderer renderer{};
// Maybe put this into 'init()' because we might use the other textures
// like snow, ice, ... and then we can parameterize this.
paradiso::Bitmap image =
paradiso::BitmapIO::get().load("PNG/groundGrass.png");
AABB aabb{};
void init() {
// The image's height is 71 px.
float scale_y = 71.0f / WINDOW_HEIGHT;
float pivot_y = -1.0f + scale_y;
sprites = {
paradiso::Sprite{
.bitmap = image,
.pivot = {paradiso::Vector2<float>::make(0.0f, pivot_y)},
.scale = {paradiso::Vector2<float>::make(1.0f, scale_y)}},
paradiso::Sprite{
.bitmap = image,
.pivot = {paradiso::Vector2<float>::make(2.0f, pivot_y)},
.scale = {paradiso::Vector2<float>::make(1.0f, scale_y)}}};
// We'll make the height half the height of the texture.
// (If you want 1:1 size, you need to double the scale of the sprite.)
aabb = AABB{.position = {paradiso::Vector2<float>::make(-1.0f, -1.0f)},
.size = {paradiso::Vector2<float>::make(2.0f, scale_y)}};
}
void update() {
for (auto& sprite : sprites) {
sprite.pivot.x() -= SCROLLING_SPEED;
if (sprite.pivot.x() <= -2.0f) {
sprite.pivot.x() += 4.0f;
}
}
}
void draw(const paradiso::Shader& shader) {
for (const auto& sprite : sprites) {
shader.set_uniform("pivot", sprite.pivot);
shader.set_uniform("scale", sprite.scale);
shader.set_uniform("rotation", sprite.rotation);
renderer.draw(sprite, shader);
}
}
};
struct Rocks {
static constexpr float SCROLLING_SPEED = 0.006f;
static constexpr float GAP_SIZE = 1.6f;
paradiso::Sprite top_sprite;
paradiso::Sprite bottom_sprite;
paradiso::Renderer top_renderer{};
paradiso::Renderer bottom_renderer{};
// Maybe put this into 'init()' because we might use the other textures
// like snow, ice, ... and then we can parameterize this.
paradiso::Bitmap top_image =
paradiso::BitmapIO::get().load("PNG/rockGrassDown.png");
paradiso::Bitmap bottom_image =
paradiso::BitmapIO::get().load("PNG/rockGrass.png");
AABB top_aabb;
AABB bottom_aabb;
void init(float x) {
// The image's size is 108x239 px.
float scale_x = 108.0f / WINDOW_WIDTH;
float scale_y = 239.0f / WINDOW_HEIGHT;
auto scale = paradiso::Vector2<float>{
paradiso::Vector2<float>::make(scale_x, scale_y)};
float top_position_y =
1.0f - scale_y + (std::rand() % 60) / 100.0f + 0.04f;
float bottom_position_y = top_position_y - GAP_SIZE;
top_sprite = paradiso::Sprite{
.bitmap = top_image,
.pivot = {paradiso::Vector2<float>::make(x, top_position_y)},
.scale = scale};
bottom_sprite = paradiso::Sprite{
.bitmap = bottom_image,
.pivot = {paradiso::Vector2<float>::make(x, bottom_position_y)},
.scale = scale};
// We'll make the width a quarter the width of the texture.
// (If you want 1:1 size, you need to double the scale of the sprite.)
auto size = paradiso::Vector2<float>{
paradiso::Vector2<float>::make(scale_x / 2, scale_y * 2)};
auto top_position =
paradiso::Vector2<float>{paradiso::Vector2<float>::make(
top_sprite.pivot.x() - size.x() / 2,
top_sprite.pivot.y() - size.y() / 2)};
auto bottom_position =
paradiso::Vector2<float>{paradiso::Vector2<float>::make(
bottom_sprite.pivot.x() - size.x() / 2,
bottom_sprite.pivot.y() - size.y() / 2)};
top_aabb = AABB{top_position, size};
bottom_aabb = AABB{bottom_position, size};
}
void update() {
top_sprite.pivot.x() -= SCROLLING_SPEED;
bottom_sprite.pivot.x() -= SCROLLING_SPEED;
// +----------+
// | |
// | |
// | # #-------- the x it is.
// | \ |
// | \ |
// +-----\----+
// \
// *------ the x we need.
// @Efficiency: We could just scroll it with the sprite.
top_aabb.position.x() = top_sprite.pivot.x() - top_aabb.size.x() / 2;
bottom_aabb.position.x() =
bottom_sprite.pivot.x() - bottom_aabb.size.x() / 2;
if (top_sprite.pivot.x() <= -1.2f) {
// Reset.
init(1.2f);
return;
}
}
void draw(const paradiso::Shader& shader) {
shader.set_uniform("pivot", top_sprite.pivot);
shader.set_uniform("scale", top_sprite.scale);
shader.set_uniform("rotation", top_sprite.rotation);
top_renderer.draw(top_sprite, shader);
shader.set_uniform("pivot", bottom_sprite.pivot);
shader.set_uniform("scale", bottom_sprite.scale);
shader.set_uniform("rotation", bottom_sprite.rotation);
bottom_renderer.draw(bottom_sprite, shader);
}
};
struct Plane {
static constexpr float GRAVITY = -0.001f;
static constexpr float JUMP_VELOCITY = 0.03f;
static constexpr float MAX_VELOCITY = 0.03f;
static constexpr float INIT_POSITION_Y = 0.0f;
static constexpr float INIT_VELOCITY_Y = 0.0f;
static constexpr float INIT_ROTATION = 0.0f;
static const unsigned int ANIM_INTERVAL = 10;
// Animation sprites
std::array<paradiso::Sprite, 3> sprites;
std::array<paradiso::Renderer, 3> renderers;
unsigned int anim_counter = 0;
// The direction where 'current_sprite' should go because the animation
// shouldn't loop.
int anim_direction = 1;
unsigned int current_sprite = 0;
float position_y = INIT_POSITION_Y;
float velocity_y = INIT_VELOCITY_Y;
float rotation = INIT_ROTATION;
AABB aabb;
void init() {
// The image's size is 88x73 px.
float scale_x = 88.0f / WINDOW_WIDTH;
float scale_y = 73.0f / WINDOW_HEIGHT;
auto pivot = paradiso::Vector2<float>{
paradiso::Vector2<float>::make(0.0f, position_y)};
auto scale = paradiso::Vector2<float>{
paradiso::Vector2<float>::make(scale_x, scale_y)};
sprites = {
paradiso::Sprite{.bitmap = paradiso::BitmapIO::get().load(
"PNG/Planes/planeRed1.png"),
.pivot = pivot,
.scale = scale},
paradiso::Sprite{.bitmap = paradiso::BitmapIO::get().load(
"PNG/Planes/planeRed2.png"),
.pivot = pivot,
.scale = scale},
paradiso::Sprite{.bitmap = paradiso::BitmapIO::get().load(
"PNG/Planes/planeRed3.png"),
.pivot = pivot,
.scale = scale},
};
// For reseting.
position_y = INIT_POSITION_Y;
velocity_y = INIT_VELOCITY_Y;
rotation = INIT_ROTATION;
// We'll make the size half the size of the texture.
// (If you want 1:1 size, you need to double the scale of the sprite.)
auto size = paradiso::Vector2<float>{
paradiso::Vector2<float>::make(scale_x, scale_y)};
// Center it.
auto position = paradiso::Vector2<float>{paradiso::Vector2<float>::make(
pivot.x() - size.x() / 2, pivot.y() - size.y() / 2)};
aabb = AABB{position, size};
}
void update(const paradiso::Window::KeyboardInputStack& input) {
if (anim_counter < ANIM_INTERVAL) {
anim_counter++;
} else {
anim_counter = 0;
current_sprite += anim_direction;
if (current_sprite >= sprites.size() - 1) {
anim_direction = -1;
} else if (current_sprite == 0) {
anim_direction = 1;
}
}
if (input.size() && input.top().key == ACTION_KEY &&
input.top().action == 1) {
velocity_y += JUMP_VELOCITY;
} else {
velocity_y += GRAVITY;
}
velocity_y = std::clamp(velocity_y, -MAX_VELOCITY, MAX_VELOCITY);
position_y += velocity_y;
// +-------------+
// | |
// | |
// | |
// | #--------- the y it is.
// | |
// | # |
// | \ |
// +--------\----+
// \
// *--- the y we need.
// @Efficiency: We could just add the velocity.
aabb.position.y() = position_y - aabb.size.y() / 2;
rotation = velocity_y * 500.0f;
}
void draw(const paradiso::Shader& shader) {
auto sprite = sprites[current_sprite];
sprite.rotation = rotation * (std::numbers::pi / 180.0f);
sprite.pivot.y() = position_y;
shader.set_uniform("pivot", sprite.pivot);
shader.set_uniform("scale", sprite.scale);
shader.set_uniform("rotation", sprite.rotation);
renderers[current_sprite].draw(sprite, shader);
}
};
struct StartText {
static const unsigned int TAP_ANIM_INTERVAL = 40;
// textGetReady.png, tapLeft.png, tapRight.png
std::array<paradiso::Sprite, 3> base_sprites;
std::array<paradiso::Renderer, 3> base_renderers;
// tap.png, tapTick.png
std::array<paradiso::Sprite, 2> tap_sprites;
std::array<paradiso::Renderer, 2> tap_renderers;
unsigned int tap_anim_counter = 0;
unsigned int current_tap_sprite = 0;
void init() {
base_sprites = {
// textGetReady.png
paradiso::Sprite{
.bitmap =
paradiso::BitmapIO::get().load("PNG/UI/textGetReady.png"),
.pivot = {paradiso::Vector2<float>::make(0.0f, 0.5f)},
// The image's size is 400x73 px.
.scale = {paradiso::Vector2<float>::make(
400.0f / WINDOW_WIDTH, 73.0f / WINDOW_HEIGHT)}},
// tapLeft.png
paradiso::Sprite{
.bitmap = paradiso::BitmapIO::get().load("PNG/UI/tapLeft.png"),
.pivot = {paradiso::Vector2<float>::make(0.5f, 0.0f)},
// The image's size is 85x42 px.
.scale = {paradiso::Vector2<float>::make(
85.0f / WINDOW_WIDTH, 42.0f / WINDOW_HEIGHT)}},
// tapRight.png
paradiso::Sprite{
.bitmap = paradiso::BitmapIO::get().load("PNG/UI/tapRight.png"),
.pivot = {paradiso::Vector2<float>::make(-0.5f, 0.0f)},
// The image's size is 85x42 px.
.scale = {paradiso::Vector2<float>::make(
85.0f / WINDOW_WIDTH, 42.0f / WINDOW_HEIGHT)}},
};
tap_sprites = {
// tap.png
paradiso::Sprite{
.bitmap = paradiso::BitmapIO::get().load("PNG/UI/tap.png"),
.pivot = {paradiso::Vector2<float>::make(0.0f, -0.5f)},
// The image's size is 59x59 px.
.scale = {paradiso::Vector2<float>::make(
59.0f / WINDOW_WIDTH, 59.0f / WINDOW_HEIGHT)}},
// tapTick.png
paradiso::Sprite{
.bitmap = paradiso::BitmapIO::get().load("PNG/UI/tapTick.png"),
.pivot = {paradiso::Vector2<float>::make(0.0f, -0.5f)},
// The image's size is 59x59 px.
.scale = {paradiso::Vector2<float>::make(
59.0f / WINDOW_WIDTH, 59.0f / WINDOW_HEIGHT)}},
};
}
void update() {
if (tap_anim_counter < TAP_ANIM_INTERVAL) {
tap_anim_counter++;
} else {
tap_anim_counter = 0;
current_tap_sprite = (current_tap_sprite + 1) % tap_sprites.size();
}
}
void draw(const paradiso::Shader& shader) {
for (unsigned int i = 0; i < base_sprites.size(); i++) {
auto sprite = base_sprites[i];
auto& renderer = base_renderers[i];
shader.set_uniform("pivot", sprite.pivot);
shader.set_uniform("scale", sprite.scale);
shader.set_uniform("rotation", sprite.rotation);
renderer.draw(sprite, shader);
}
auto tap_sprite = tap_sprites[current_tap_sprite];
auto& tap_renderer = tap_renderers[current_tap_sprite];
shader.set_uniform("pivot", tap_sprite.pivot);
shader.set_uniform("scale", tap_sprite.scale);
shader.set_uniform("rotation", tap_sprite.rotation);
tap_renderer.draw(tap_sprite, shader);
}
};
struct GameOverText {
paradiso::Sprite sprite;
paradiso::Renderer renderer{};
paradiso::Bitmap image =
paradiso::BitmapIO::get().load("PNG/UI/textGameOver.png");
void init() {
// The image's size is 412x78 px.
float scale_x = 412.0f / WINDOW_WIDTH;
float scale_y = 78.0f / WINDOW_HEIGHT;
sprite = {.bitmap = image,
.pivot = {paradiso::Vector2<float>::make(0.0f, 0.0f)},
.scale = {paradiso::Vector2<float>::make(scale_x, scale_y)}};
}
void update() {
// @ToDo: Make the sprite slide down.
}
void draw(const paradiso::Shader& shader) {
shader.set_uniform("pivot", sprite.pivot);
shader.set_uniform("scale", sprite.scale);
shader.set_uniform("rotation", sprite.rotation);
renderer.draw(sprite, shader);
}
};
enum class GameState { Start, Playing, GameOver };
auto main() -> int {
std::srand(std::time(nullptr));
auto canvas_size = paradiso::Size{.width = WINDOW_WIDTH * WINDOW_SCALE,
.height = WINDOW_HEIGHT * WINDOW_SCALE};
// Unser Fenster, auf das wir rendern.
auto window = paradiso::Window();
window
.set_size(canvas_size) // ... Größe
.set_position(paradiso::Point{.x = 200, .y = 200}) // ... Position
.set_title("ParadiSO.TappyPlane") // ... Titel
.set_visible(true); // ... und jetzt anzeigen!
// Der Fenster Context.
auto context = paradiso::Context{};
// Ein Shader (Schattierungsprogramm).
auto shader = paradiso::Shader{};
// Wir nutzen einen vorgefertigten Shader, der speziell für Sprites ist.
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öße eingestellt
// werden.
context.set_viewport(paradiso::Rectangle{
.position = paradiso::Point{.x = 0, .y = 0}, .size = canvas_size});
// Conflower blue
context.set_clearcolor(paradiso::RGBA::from_rgb(0x64, 0x95, 0xED));
// Der Asset Loader bekommt den Pfad.
paradiso::BitmapIO::get().set_path("assets");
// Wir initialisieren unsere Sachen.
auto game_state = GameState::Start;
auto background = Background{};
auto ground = Ground{};
auto rocks1 = Rocks{};
auto rocks2 = Rocks{};
auto plane = Plane{};
auto start_text = StartText{};
auto game_over_text = GameOverText{};
background.init();
ground.init();
rocks1.init(1.2f);
rocks2.init(2.4f);
plane.init();
start_text.init();
game_over_text.init();
auto top_aabb =
AABB{.position = {paradiso::Vector2<float>::make(-1.0f, 1.0f)},
.size = {paradiso::Vector2<float>::make(2.0f, 0.5f)}};
while (window.update([&](paradiso::Window& window) -> bool {
auto t1 = std::chrono::high_resolution_clock::now();
if (window.keyboard_input().size() &&
window.keyboard_input().top().key == 'Q') {
// Update-Loop beenden.
return false;
}
if (game_state == GameState::Start) {
if (window.keyboard_input().size() &&
window.keyboard_input().top().key == ACTION_KEY &&
window.keyboard_input().top().action == 1) {
game_state = GameState::Playing;
// Make the plane jump for the first time.
// @ToDo: The plane jumps too high because there was no gravity
// before.
plane.update(window.keyboard_input());
}
background.update();
ground.update();
start_text.update();
} else if (game_state == GameState::Playing) {
background.update();
ground.update();
rocks1.update();
rocks2.update();
plane.update(window.keyboard_input());
if (plane.aabb.is_colliding_with(rocks1.top_aabb) ||
plane.aabb.is_colliding_with(rocks1.bottom_aabb) ||
plane.aabb.is_colliding_with(rocks2.top_aabb) ||
plane.aabb.is_colliding_with(rocks2.bottom_aabb) ||
plane.aabb.is_colliding_with(top_aabb) ||
plane.aabb.is_colliding_with(ground.aabb)) {
game_state = GameState::GameOver;
}
} else if (game_state == GameState::GameOver) {
if (window.keyboard_input().size() &&
window.keyboard_input().top().key == ACTION_KEY) {
// Reset everything.
background.init();
ground.init();
rocks1.init(1.2f);
rocks2.init(2.4f);
plane.init();
game_state = GameState::Start;
}
}
context.set_viewport(paradiso::Rectangle{
.position = paradiso::Point{.x = 0, .y = 0}, .size = canvas_size});
context.clear();
background.draw(shader);
plane.draw(shader);
rocks1.draw(shader);
rocks2.draw(shader);
ground.draw(shader);
// Kinda dumb that we check the game state here the second time but I
// like to seperate 'update' from 'draw'.
if (game_state == GameState::Start) {
start_text.draw(shader);
} else if (game_state == GameState::GameOver) {
game_over_text.draw(shader);
}
// Einen kurzen Moment warten, um auf 60 FPS zu kommen.
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);
return true;
})) {
};
return 0;
}