/** * paradiso - Paradigmen der Softwareentwicklung * * (c) Copyright 2023-2025 Hartmut Seichter and Contributors * */ #include #include #include #include #include #include #include #include #include #include #include #include #include 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 = ' '; static constexpr float SCROLLING_SPEED_INC_FACTOR = 1.0002f; // x,y+h --- x+w,y+h // | | // | | // | | // x,y ----- x+w,y struct AABB { paradiso::Vector2 position{}; paradiso::Vector2 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 INIT_SCROLLING_SPEED = 0.003f; float scrolling_speed = INIT_SCROLLING_SPEED; std::array sprites; paradiso::Renderer renderer{}; paradiso::Bitmap image = paradiso::BitmapIO::get().load("PNG/background.png"); void init() { // For resetting. scrolling_speed = INIT_SCROLLING_SPEED; sprites = {paradiso::Sprite{ .bitmap = image, .pivot = {paradiso::Vector2::make(0.0f, 0.0f)}, .scale = {paradiso::Vector2::make(1.0f, 1.0f)}}, paradiso::Sprite{ .bitmap = image, .pivot = {paradiso::Vector2::make(2.0f, 0.0f)}, .scale = {paradiso::Vector2::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 INIT_SCROLLING_SPEED = 0.009f; float scrolling_speed = INIT_SCROLLING_SPEED; std::array 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() { // For resetting. scrolling_speed = INIT_SCROLLING_SPEED; // 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::make(0.0f, pivot_y)}, .scale = {paradiso::Vector2::make(1.0f, scale_y)}}, paradiso::Sprite{ .bitmap = image, .pivot = {paradiso::Vector2::make(2.0f, pivot_y)}, .scale = {paradiso::Vector2::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::make(-1.0f, -1.0f)}, .size = {paradiso::Vector2::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 INIT_SCROLLING_SPEED = 0.006f; static constexpr float GAP_SIZE = 1.6f; float scrolling_speed = INIT_SCROLLING_SPEED; 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, bool reset_scrolling_speed = false) { if (reset_scrolling_speed) { scrolling_speed = INIT_SCROLLING_SPEED; } // 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{ paradiso::Vector2::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::make(x, top_position_y)}, .scale = scale}; bottom_sprite = paradiso::Sprite{ .bitmap = bottom_image, .pivot = {paradiso::Vector2::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{ paradiso::Vector2::make(scale_x / 2, scale_y * 2)}; auto top_position = paradiso::Vector2{paradiso::Vector2::make( top_sprite.pivot.x() - size.x() / 2, top_sprite.pivot.y() - size.y() / 2)}; auto bottom_position = paradiso::Vector2{paradiso::Vector2::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 sprites; std::array 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{ paradiso::Vector2::make(0.0f, position_y)}; auto scale = paradiso::Vector2{ paradiso::Vector2::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 resetting. 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{ paradiso::Vector2::make(scale_x, scale_y)}; // Center it. auto position = paradiso::Vector2{paradiso::Vector2::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 base_sprites; std::array base_renderers; // tap.png, tapTick.png std::array tap_sprites; std::array 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::make(0.0f, 0.5f)}, // The image's size is 400x73 px. .scale = {paradiso::Vector2::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::make(0.5f, 0.0f)}, // The image's size is 85x42 px. .scale = {paradiso::Vector2::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::make(-0.5f, 0.0f)}, // The image's size is 85x42 px. .scale = {paradiso::Vector2::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::make(0.0f, -0.5f)}, // The image's size is 59x59 px. .scale = {paradiso::Vector2::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::make(0.0f, -0.5f)}, // The image's size is 59x59 px. .scale = {paradiso::Vector2::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::make(0.0f, 0.0f)}, .scale = {paradiso::Vector2::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, true); // We don't need to set this to true, rocks2.init(2.4f, true); // but why not? plane.init(); start_text.init(); game_over_text.init(); auto top_aabb = AABB{.position = {paradiso::Vector2::make(-1.0f, 1.0f)}, .size = {paradiso::Vector2::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()); background.scrolling_speed *= SCROLLING_SPEED_INC_FACTOR; ground.scrolling_speed *= SCROLLING_SPEED_INC_FACTOR; rocks1.scrolling_speed *= SCROLLING_SPEED_INC_FACTOR; rocks2.scrolling_speed *= SCROLLING_SPEED_INC_FACTOR; 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, true); rocks2.init(2.4f, true); 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(t2 - t1); auto wait = std::chrono::microseconds(1000000 / FRAME_RATE) - duration; std::this_thread::sleep_for(wait); return true; })) { }; return 0; }