From 2d7277efeca8f054f07c06b6dff2527d0621a22b Mon Sep 17 00:00:00 2001 From: brxxh <11dac2t@huhn-online.de> Date: Fri, 30 May 2025 19:57:30 +0200 Subject: [PATCH] Add game state and some UI --- examples/tappyplane/tappyplane.cpp | 299 +++++++++++++++++++++++------ 1 file changed, 236 insertions(+), 63 deletions(-) diff --git a/examples/tappyplane/tappyplane.cpp b/examples/tappyplane/tappyplane.cpp index cda1265..4575f67 100644 --- a/examples/tappyplane/tappyplane.cpp +++ b/examples/tappyplane/tappyplane.cpp @@ -25,6 +25,7 @@ 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 // | | @@ -97,7 +98,7 @@ struct Ground { void init() { // The image's height is 71 px. - float scale_y = 71.0f / (float)WINDOW_HEIGHT; + float scale_y = 71.0f / WINDOW_HEIGHT; float pivot_y = -1.0f + scale_y; @@ -113,10 +114,8 @@ struct Ground { // 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)} - }; + aabb = AABB{.position = {paradiso::Vector2::make(-1.0f, -1.0f)}, + .size = {paradiso::Vector2::make(2.0f, scale_y)}}; } void update() { @@ -160,13 +159,13 @@ struct Rocks { void init(float x) { // The image's size is 108x239 px. - float scale_x = 108.0f / (float)WINDOW_WIDTH; - float scale_y = 239.0f / (float)WINDOW_HEIGHT; + 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 + ((float)(std::rand() % 60) / 100) + 0.04f; + 1.0f - scale_y + (std::rand() % 60) / 100.0f + 0.04f; float bottom_position_y = top_position_y - GAP_SIZE; top_sprite = paradiso::Sprite{ @@ -179,10 +178,10 @@ struct Rocks { .pivot = {paradiso::Vector2::make(x, bottom_position_y)}, .scale = scale}; - // We'll make the width half the width of the texture. + // 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, scale_y * 2)}; + paradiso::Vector2::make(scale_x / 2, scale_y * 2)}; auto top_position = paradiso::Vector2{paradiso::Vector2::make( @@ -204,11 +203,13 @@ struct Rocks { // +----------+ // | | + // | | // | # #-------- the x it is. // | \ | - // +----\-----+ - // \ - // *------- the x we need. + // | \ | + // +-----\----+ + // \ + // *------ 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; @@ -236,15 +237,20 @@ struct Rocks { }; struct Plane { - static const unsigned int SPRITE_COUNT = 3; static constexpr float GRAVITY = -0.001f; static constexpr float JUMP_VELOCITY = 0.03f; static constexpr float MAX_VELOCITY = 0.03f; - std::array sprites; - std::array renderers; + 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_interval = 10; unsigned int anim_counter = 0; // The direction where 'current_sprite' should go because the animation @@ -253,17 +259,16 @@ struct Plane { unsigned int current_sprite = 0; - float position_y = 0.5f; - float rotation = 0.0f; - - float velocity_y = 0.0f; + 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 / (float)WINDOW_WIDTH; - float scale_y = 73.0f / (float)WINDOW_HEIGHT; + 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)}; @@ -285,6 +290,11 @@ struct Plane { .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{ @@ -298,20 +308,21 @@ struct Plane { } void update(const paradiso::Window::KeyboardInputStack& input) { - if (anim_counter < anim_interval) { + if (anim_counter < ANIM_INTERVAL) { anim_counter++; } else { anim_counter = 0; current_sprite += anim_direction; - if (current_sprite == SPRITE_COUNT - 1) { + if (current_sprite >= sprites.size() - 1) { anim_direction = -1; } else if (current_sprite == 0) { anim_direction = 1; } } - if (input.size() && input.top().key == ' ' && input.top().action == 1) { + if (input.size() && input.top().key == ACTION_KEY && + input.top().action == 1) { velocity_y += JUMP_VELOCITY; } else { velocity_y += GRAVITY; @@ -320,13 +331,17 @@ struct Plane { velocity_y = std::clamp(velocity_y, -MAX_VELOCITY, MAX_VELOCITY); position_y += velocity_y; - // +---------+ - // | | - // | #------- the y it is. - // | | - // +----#----+ - // \ - // *----- the y we need. + // +-------------+ + // | | + // | | + // | | + // | #--------- the y it is. + // | | + // | # | + // | \ | + // +--------\----+ + // \ + // *--- the y we need. // @Efficiency: We could just add the velocity. aabb.position.y() = position_y - aabb.size.y() / 2; @@ -346,6 +361,125 @@ struct Plane { } }; +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)); @@ -382,45 +516,82 @@ auto main() -> int { paradiso::BitmapIO::get().set_path("assets"); // Wir initialisieren unsere Sachen. + auto game_state = GameState::Start; + auto background = Background{}; - background.init(); - auto ground = Ground{}; - ground.init(); - auto rocks1 = Rocks{}; - rocks1.init(1.2f); - auto rocks2 = Rocks{}; - rocks2.init(2.4f); - 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::make(-1.0f, 1.0f)}, .size = {paradiso::Vector2::make(2.0f, 0.5f)}}; while (window.update([&](paradiso::Window& window) -> bool { - background.update(); - ground.update(); + auto t1 = std::chrono::high_resolution_clock::now(); - 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)) { - - std::cout << "COLLIDING" << std::endl; + if (window.keyboard_input().size() && + window.keyboard_input().top().key == 'Q') { + // Update-Loop beenden. + return false; } - auto t1 = std::chrono::high_resolution_clock::now(); + 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}); @@ -435,6 +606,14 @@ auto main() -> int { 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 = @@ -442,12 +621,6 @@ auto main() -> int { auto wait = std::chrono::microseconds(1000000 / FRAME_RATE) - duration; std::this_thread::sleep_for(wait); - if (window.keyboard_input().size() && - window.keyboard_input().top().key == 'Q') { - // Update-Loop beenden. - return false; - } - return true; })) { };