forked from Hartmut/paradiso
799 lines
27 KiB
C++
799 lines
27 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 <print>
|
|
#include <thread>
|
|
#include <tuple>
|
|
#include <unordered_map>
|
|
|
|
namespace TappyPlane {
|
|
|
|
using SpriteName = std::tuple<std::string_view, std::string_view>;
|
|
using SpriteMap = std::unordered_map<std::string_view, paradiso::Bitmap>;
|
|
|
|
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<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 INIT_SCROLLING_SPEED = 0.003f;
|
|
|
|
float scrolling_speed = INIT_SCROLLING_SPEED;
|
|
|
|
std::array<paradiso::Sprite, 2> sprites;
|
|
paradiso::Renderer renderer{};
|
|
|
|
void init(SpriteMap& sprite_map) {
|
|
sprites = {paradiso::Sprite{
|
|
.bitmap = sprite_map["background"],
|
|
.pivot = {paradiso::Vector2<float>::make(0.0f, 0.0f)},
|
|
.scale = {paradiso::Vector2<float>::make(1.0f, 1.0f)}},
|
|
paradiso::Sprite{
|
|
.bitmap = sprite_map["background"],
|
|
.pivot = {paradiso::Vector2<float>::make(2.0f, 0.0f)},
|
|
.scale = {paradiso::Vector2<float>::make(1.0f, 1.0f)}}};
|
|
}
|
|
|
|
void reset() {
|
|
scrolling_speed = INIT_SCROLLING_SPEED;
|
|
|
|
sprites[0].pivot.x() = 0.0f;
|
|
sprites[1].pivot.x() = 2.0f;
|
|
}
|
|
|
|
void update() {
|
|
scrolling_speed *= SCROLLING_SPEED_INC_FACTOR;
|
|
|
|
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<paradiso::Sprite, 2> sprites;
|
|
paradiso::Renderer renderer{};
|
|
|
|
AABB aabb{};
|
|
|
|
void init(SpriteMap& sprite_map) {
|
|
// 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 = sprite_map["ground"],
|
|
.pivot = {paradiso::Vector2<float>::make(0.0f, pivot_y)},
|
|
.scale = {paradiso::Vector2<float>::make(1.0f, scale_y)}},
|
|
paradiso::Sprite{
|
|
.bitmap = sprite_map["ground"],
|
|
.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 reset() {
|
|
scrolling_speed = INIT_SCROLLING_SPEED;
|
|
|
|
sprites[0].pivot.x() = 0.0f;
|
|
sprites[1].pivot.x() = 2.0f;
|
|
}
|
|
|
|
void update() {
|
|
scrolling_speed *= SCROLLING_SPEED_INC_FACTOR;
|
|
|
|
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.5f;
|
|
|
|
// The image's size is 108x239 px.
|
|
static constexpr float SCALE_X = 108.0f / WINDOW_WIDTH;
|
|
static constexpr float SCALE_Y = 239.0f / WINDOW_HEIGHT;
|
|
|
|
static constexpr paradiso::Vector2<float> SCALE{
|
|
paradiso::Vector2<float>::make(SCALE_X, SCALE_Y)};
|
|
|
|
float scrolling_speed = INIT_SCROLLING_SPEED;
|
|
|
|
paradiso::Sprite top_sprite;
|
|
paradiso::Sprite bottom_sprite;
|
|
|
|
paradiso::Renderer top_renderer{};
|
|
paradiso::Renderer bottom_renderer{};
|
|
|
|
AABB top_aabb;
|
|
AABB bottom_aabb;
|
|
|
|
float init_pivot_x = 0.0f;
|
|
|
|
auto get_random_top_position_y() -> float {
|
|
return 1.0f - SCALE_Y + (std::rand() % 50) / 100.0f + 0.01f;
|
|
}
|
|
|
|
void init_aabbs() {
|
|
// 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 init(SpriteMap& sprite_map, float pivot_x) {
|
|
init_pivot_x = pivot_x;
|
|
|
|
float top_position_y = get_random_top_position_y();
|
|
float bottom_position_y = top_position_y - GAP_SIZE;
|
|
|
|
top_sprite = paradiso::Sprite{.bitmap = sprite_map["rocks.top"],
|
|
.pivot = {paradiso::Vector2<float>::make(
|
|
init_pivot_x, top_position_y)},
|
|
.scale = SCALE};
|
|
|
|
bottom_sprite =
|
|
paradiso::Sprite{.bitmap = sprite_map["rocks.bottom"],
|
|
.pivot = {paradiso::Vector2<float>::make(
|
|
init_pivot_x, bottom_position_y)},
|
|
.scale = SCALE};
|
|
|
|
init_aabbs();
|
|
}
|
|
|
|
void reset() {
|
|
scrolling_speed = INIT_SCROLLING_SPEED;
|
|
|
|
float top_position_y = get_random_top_position_y();
|
|
float bottom_position_y = top_position_y - GAP_SIZE;
|
|
|
|
top_sprite.pivot = paradiso::Vector2<float>{
|
|
paradiso::Vector2<float>::make(init_pivot_x, top_position_y)};
|
|
bottom_sprite.pivot = paradiso::Vector2<float>{
|
|
paradiso::Vector2<float>::make(init_pivot_x, bottom_position_y)};
|
|
|
|
init_aabbs();
|
|
}
|
|
|
|
void jump_back() {
|
|
float top_position_y = get_random_top_position_y();
|
|
float bottom_position_y = top_position_y - GAP_SIZE;
|
|
|
|
top_sprite.pivot = paradiso::Vector2<float>{
|
|
paradiso::Vector2<float>::make(1.2f, top_position_y)};
|
|
bottom_sprite.pivot = paradiso::Vector2<float>{
|
|
paradiso::Vector2<float>::make(1.2f, bottom_position_y)};
|
|
|
|
init_aabbs();
|
|
}
|
|
|
|
void update() {
|
|
scrolling_speed *= SCROLLING_SPEED_INC_FACTOR;
|
|
|
|
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 the AABBs with the sprites.
|
|
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.
|
|
jump_back();
|
|
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 constexpr paradiso::Vector2<float> INIT_PIVOT{
|
|
paradiso::Vector2<float>::make(0.0f, INIT_POSITION_Y)};
|
|
|
|
static const unsigned int ANIM_INTERVAL = 10;
|
|
|
|
// The image's size is 88x73 px.
|
|
static constexpr float SCALE_X = 88.0f / WINDOW_WIDTH;
|
|
static constexpr float SCALE_Y = 73.0f / WINDOW_HEIGHT;
|
|
|
|
static constexpr paradiso::Vector2<float> SCALE{
|
|
paradiso::Vector2<float>::make(SCALE_X, SCALE_Y)};
|
|
|
|
// 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_aabb() {
|
|
// 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.)
|
|
// We could use 'SCALE' here, but we'll keep this for clarity.
|
|
// (Although, now we no longer need 'SCALE' as a constant in this entire struct.)
|
|
auto size = paradiso::Vector2<float>{
|
|
paradiso::Vector2<float>::make(SCALE_X, SCALE_Y)};
|
|
|
|
// Center it.
|
|
auto position = paradiso::Vector2<float>{paradiso::Vector2<float>::make(
|
|
INIT_PIVOT.x() - size.x() / 2, INIT_PIVOT.y() - size.y() / 2)};
|
|
|
|
aabb = AABB{position, size};
|
|
}
|
|
|
|
void init(SpriteMap& sprite_map) {
|
|
sprites = {paradiso::Sprite{.bitmap = sprite_map["plane.1"],
|
|
.pivot = INIT_PIVOT,
|
|
.scale = SCALE},
|
|
paradiso::Sprite{.bitmap = sprite_map["plane.2"],
|
|
.pivot = INIT_PIVOT,
|
|
.scale = SCALE},
|
|
paradiso::Sprite{.bitmap = sprite_map["plane.3"],
|
|
.pivot = INIT_PIVOT,
|
|
.scale = SCALE}};
|
|
|
|
init_aabb();
|
|
}
|
|
|
|
void reset() {
|
|
for (auto& sprite : sprites) {
|
|
sprite.pivot = INIT_PIVOT;
|
|
}
|
|
|
|
position_y = INIT_POSITION_Y;
|
|
velocity_y = INIT_VELOCITY_Y;
|
|
rotation = INIT_ROTATION;
|
|
|
|
init_aabb();
|
|
}
|
|
|
|
void update_animation() {
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
|
|
void update(const paradiso::Window::KeyboardInputStack& input) {
|
|
update_animation();
|
|
|
|
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 to the AABB's y position.
|
|
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 StartUI {
|
|
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(SpriteMap& sprite_map) {
|
|
base_sprites = {
|
|
paradiso::Sprite{
|
|
.bitmap = sprite_map["start_ui.text"],
|
|
.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)}},
|
|
paradiso::Sprite{
|
|
.bitmap = sprite_map["tap_sign.left"],
|
|
.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)}},
|
|
paradiso::Sprite{
|
|
.bitmap = sprite_map["tap_sign.right"],
|
|
.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 = {
|
|
paradiso::Sprite{
|
|
.bitmap = sprite_map["tap.normal"],
|
|
.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)}},
|
|
paradiso::Sprite{
|
|
.bitmap = sprite_map["tap.tick"],
|
|
.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 GameOverUI {
|
|
paradiso::Sprite sprite;
|
|
paradiso::Renderer renderer{};
|
|
|
|
void init(SpriteMap& sprite_map) {
|
|
// The image's size is 412x78 px.
|
|
float scale_x = 412.0f / WINDOW_WIDTH;
|
|
float scale_y = 78.0f / WINDOW_HEIGHT;
|
|
|
|
sprite = {.bitmap = sprite_map["game_over_ui.text"],
|
|
.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 };
|
|
|
|
struct App {
|
|
SpriteMap sprites;
|
|
|
|
enum class WorldType { Dirt, Grass, Ice, Rock, Snow };
|
|
enum class PlaneType { Blue, Green, Red, Yellow };
|
|
|
|
static auto create(WorldType world_type = WorldType::Grass, PlaneType plane_type = PlaneType::Red) -> App {
|
|
auto app = App{};
|
|
|
|
paradiso::BitmapIO::get().set_path(
|
|
paradiso::get_executable_path().parent_path().string() + "/assets");
|
|
|
|
// Yes, I'm lazy, how did you know?
|
|
const char *ground_filename = nullptr;
|
|
const char *rocks_top_filename = nullptr;
|
|
const char *rocks_bottom_filename = nullptr;
|
|
switch (world_type) {
|
|
case WorldType::Dirt:
|
|
ground_filename = "PNG/groundDirt.png";
|
|
rocks_top_filename = "PNG/rockDown.png";
|
|
rocks_bottom_filename = "PNG/rock.png";
|
|
break;
|
|
case WorldType::Ice:
|
|
ground_filename = "PNG/groundIce.png";
|
|
rocks_top_filename = "PNG/rockIceDown.png";
|
|
rocks_bottom_filename = "PNG/rockIce.png";
|
|
break;
|
|
case WorldType::Rock:
|
|
ground_filename = "PNG/groundRock.png";
|
|
rocks_top_filename = "PNG/rockDown.png";
|
|
rocks_bottom_filename = "PNG/rock.png";
|
|
break;
|
|
case WorldType::Snow:
|
|
ground_filename = "PNG/groundSnow.png";
|
|
rocks_top_filename = "PNG/rockSnowDown.png";
|
|
rocks_bottom_filename = "PNG/rockSnow.png";
|
|
break;
|
|
default:
|
|
case WorldType::Grass:
|
|
ground_filename = "PNG/groundGrass.png";
|
|
rocks_top_filename = "PNG/rockGrassDown.png";
|
|
rocks_bottom_filename = "PNG/rockGrass.png";
|
|
break;
|
|
}
|
|
|
|
// Yes, I'm lazy, how did you know?
|
|
const char *plane_1_filename = nullptr;
|
|
const char *plane_2_filename = nullptr;
|
|
const char *plane_3_filename = nullptr;
|
|
switch (plane_type) {
|
|
case PlaneType::Blue:
|
|
plane_1_filename = "PNG/Planes/planeBlue1.png";
|
|
plane_2_filename = "PNG/Planes/planeBlue2.png";
|
|
plane_3_filename = "PNG/Planes/planeBlue3.png";
|
|
break;
|
|
case PlaneType::Green:
|
|
plane_1_filename = "PNG/Planes/planeGreen1.png";
|
|
plane_2_filename = "PNG/Planes/planeGreen2.png";
|
|
plane_3_filename = "PNG/Planes/planeGreen3.png";
|
|
break;
|
|
case PlaneType::Yellow:
|
|
plane_1_filename = "PNG/Planes/planeYellow1.png";
|
|
plane_2_filename = "PNG/Planes/planeYellow2.png";
|
|
plane_3_filename = "PNG/Planes/planeYellow3.png";
|
|
break;
|
|
default:
|
|
case PlaneType::Red:
|
|
plane_1_filename = "PNG/Planes/planeRed1.png";
|
|
plane_2_filename = "PNG/Planes/planeRed2.png";
|
|
plane_3_filename = "PNG/Planes/planeRed3.png";
|
|
break;
|
|
}
|
|
|
|
auto assets = std::array{
|
|
SpriteName{"background", "PNG/background.png"},
|
|
SpriteName{"ground", ground_filename},
|
|
SpriteName{"rocks.top", rocks_top_filename},
|
|
SpriteName{"rocks.bottom", rocks_bottom_filename},
|
|
SpriteName{"plane.1", plane_1_filename},
|
|
SpriteName{"plane.2", plane_2_filename},
|
|
SpriteName{"plane.3", plane_3_filename},
|
|
SpriteName{"tap_sign.left", "PNG/UI/tapRight.png"},
|
|
SpriteName{"tap_sign.right", "PNG/UI/tapLeft.png"},
|
|
SpriteName{"tap.normal", "PNG/UI/tap.png"},
|
|
SpriteName{"tap.tick", "PNG/UI/tapTick.png"},
|
|
SpriteName{"start_ui.text", "PNG/UI/textGetReady.png"},
|
|
SpriteName{"game_over_ui.text", "PNG/UI/textGameOver.png"},
|
|
};
|
|
|
|
for (const auto& [name, filename] : assets) {
|
|
std::print("{} : {} -> ", name, filename);
|
|
|
|
auto bitmap = paradiso::BitmapIO::get().load(filename);
|
|
app.sprites[name] = bitmap;
|
|
}
|
|
|
|
return app;
|
|
}
|
|
|
|
void run() {
|
|
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));
|
|
|
|
// 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_ui = StartUI{};
|
|
auto game_over_ui = GameOverUI{};
|
|
|
|
background.init(sprites);
|
|
ground.init(sprites);
|
|
rocks1.init(sprites, 1.2f);
|
|
rocks2.init(sprites, 2.4f);
|
|
plane.init(sprites);
|
|
start_ui.init(sprites);
|
|
game_over_ui.init(sprites);
|
|
|
|
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();
|
|
plane.update_animation(); // Only the animation.
|
|
ground.update();
|
|
start_ui.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.reset();
|
|
ground.reset();
|
|
rocks1.reset();
|
|
rocks2.reset();
|
|
plane.reset();
|
|
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_ui.draw(shader);
|
|
} else if (game_state == GameState::GameOver) {
|
|
game_over_ui.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;
|
|
})) {
|
|
};
|
|
}
|
|
};
|
|
|
|
} // namespace TappyPlane
|
|
|
|
auto main() -> int {
|
|
std::srand(std::time(nullptr));
|
|
|
|
auto world_type = TappyPlane::App::WorldType::Grass;
|
|
auto plane_type = TappyPlane::App::PlaneType::Red;
|
|
|
|
auto app = TappyPlane::App::create(world_type, plane_type);
|
|
app.run();
|
|
return 0;
|
|
}
|