diff --git a/src/main.cpp b/src/main.cpp index cd75f31..33ec931 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,7 +1,5 @@ -#include "samples/collecting_game.h" -#include "samples/fighting_game.h" +#include "samples/ghhb_game.h" #include "samples/title_screen.h" -#include "samples/zombie_game.h" // Emscripten is used for web builds. #ifdef __EMSCRIPTEN__ @@ -19,8 +17,7 @@ void update() // TODO: Make this a GUI app for each platform. int main(int argc, char** argv) { - // Initialize the window - game.add_manager(1280, 720, "Game Jam Kit"); + game.add_manager(1280, 720, "Guitar Hero But Better"); auto font_manager = game.add_manager(); game.init(); @@ -30,11 +27,8 @@ int main(int argc, char** argv) font_manager->set_texture_filter("Roboto", TEXTURE_FILTER_BILINEAR); game.add_scene("title"); - game.add_scene("fighting"); - game.add_scene("collecting"); - game.add_scene("zombie"); + game.add_scene("ghhb"); -// Main game loop #ifdef __EMSCRIPTEN__ emscripten_set_main_loop(update, 0, true); #else diff --git a/src/samples/collecting_game.h b/src/samples/collecting_game.h deleted file mode 100644 index f7316aa..0000000 --- a/src/samples/collecting_game.h +++ /dev/null @@ -1,577 +0,0 @@ -/** - * Demonstration of split screen camera system and sensors for collecting items. - */ - -#pragma once - -#include "engine/prefabs/includes.h" - -/** - * A basic collecting character. - * See also engine/prefabs/game_objects.h for PlatformerCharacter. - */ -class CollectingCharacter : public GameObject -{ -public: - CharacterParams p; - PhysicsService* physics; - LevelService* level; - BodyComponent* body; - PlatformerMovementComponent* movement; - AnimationController* animation; - MultiComponent* sounds; - SoundComponent* jump_sound; - SoundComponent* die_sound; - int score = 0; - - bool grounded = false; - bool on_wall_left = false; - bool on_wall_right = false; - float coyote_timer = 0.0f; - float jump_buffer_timer = 0.0f; - int gamepad = 0; - int player_number = 1; - float width = 24.0f; - float height = 24.0f; - - CollectingCharacter(CharacterParams p, int player_number = 1) : - p(p), - player_number(player_number), - gamepad(player_number - 1), - width(p.width), - height(p.height) - { - } - - void init() override - { - - // Grab the physics service. - // All get_service calls should be done in init(). get_service is not quick and this also allows us to test - // that all services exist during init time. - physics = scene->get_service(); - - // Setup the character's physics body using the BodyComponent initialization callback lambda. - body = add_component( - [=](BodyComponent& b) - { - b2BodyDef body_def = b2DefaultBodyDef(); - body_def.type = b2_dynamicBody; - body_def.fixedRotation = true; - body_def.isBullet = true; - // All units in box2d are in meters. - body_def.position = physics->convert_to_meters(p.position); - // Assign this GameObject as the user data so we can find it in collision callbacks. - body_def.userData = this; - b.id = b2CreateBody(physics->world, &body_def); - - b2SurfaceMaterial body_material = b2DefaultSurfaceMaterial(); - body_material.friction = p.friction; - body_material.restitution = p.restitution; - - b2ShapeDef box_shape_def = b2DefaultShapeDef(); - box_shape_def.density = p.density; - box_shape_def.material = body_material; - - // Needed to get sensor events. - box_shape_def.enableSensorEvents = true; - - // We use a rounded box which helps with getting stuck on edges. - b2Polygon body_polygon = b2MakeRoundedBox(physics->convert_to_meters(p.width / 2.0f), - physics->convert_to_meters(p.height / 2.0f), - physics->convert_to_meters(0.25)); - b2CreatePolygonShape(b.id, &box_shape_def, &body_polygon); - }); - - PlatformerMovementParams mp; - mp.width = p.width; - mp.height = p.height; - movement = add_component(mp); - - level = scene->get_service(); - - // TODO: Is only allowing one component per type really as cool an idea as I thought? - sounds = add_component>(); - jump_sound = sounds->add_component("jump", "assets/sounds/jump.wav"); - die_sound = sounds->add_component("die", "assets/sounds/die.wav"); - - // Setup animations. - animation = add_component(body); - if (player_number == 1) - { - animation->add_animation("run", - std::vector{"assets/pixel_platformer/characters/green_1.png", - "assets/pixel_platformer/characters/green_2.png"}, - 10.0f); - } - else if (player_number == 2) - { - animation->add_animation("run", - std::vector{"assets/pixel_platformer/characters/blue_1.png", - "assets/pixel_platformer/characters/blue_2.png"}, - 10.0f); - } - else if (player_number == 3) - { - animation->add_animation("run", - std::vector{"assets/pixel_platformer/characters/pink_1.png", - "assets/pixel_platformer/characters/pink_2.png"}, - 10.0f); - } - else if (player_number == 4) - { - animation->add_animation("run", - std::vector{"assets/pixel_platformer/characters/yellow_1.png", - "assets/pixel_platformer/characters/yellow_2.png"}, - 10.0f); - } - } - - void update(float delta_time) override - { - // Get input and route to movement component. - float deadzone = 0.1f; - - const bool jump_pressed = - IsKeyPressed(KEY_W) || IsGamepadButtonPressed(gamepad, GAMEPAD_BUTTON_RIGHT_FACE_DOWN); - const bool jump_held = IsKeyDown(KEY_W) || IsGamepadButtonDown(gamepad, GAMEPAD_BUTTON_RIGHT_FACE_DOWN); - - float move_x = 0.0f; - move_x = GetGamepadAxisMovement(gamepad, GAMEPAD_AXIS_LEFT_X); - if (fabsf(move_x) < deadzone) - { - move_x = 0.0f; - } - if (IsKeyDown(KEY_D) || IsGamepadButtonDown(gamepad, GAMEPAD_BUTTON_LEFT_FACE_RIGHT)) - { - move_x = 1.0f; - } - else if (IsKeyDown(KEY_A) || IsGamepadButtonDown(gamepad, GAMEPAD_BUTTON_LEFT_FACE_LEFT)) - { - move_x = -1.0f; - } - - movement->set_input(move_x, jump_pressed, jump_held); - - if (movement->grounded && jump_pressed) - { - jump_sound->play(); - } - - if (fabsf(movement->move_x) > 0.1f) - { - animation->play("run"); - animation->flip_x = movement->move_x > 0.0f; - } - else - { - animation->pause(); - } - } - - void die() - { - // Re-spawn at start position. - body->set_position(p.position); - body->set_velocity(Vector2{0.0f, 0.0f}); - die_sound->play(); - } -}; - -enum EnemyType -{ - Bat, - DrillHead, - BlockHead -}; - -/** - * An enemy. - */ -class Enemy : public GameObject -{ -public: - Vector2 start; - Vector2 end; - PhysicsService* physics; - BodyComponent* body; - AnimationController* animation; - EnemyType type; - float radius = 12.0f; - - Enemy(EnemyType type, Vector2 start, Vector2 end) : type(type), start(start), end(end) {} - void init_object() override - { - physics = scene->get_service(); - - body = add_component( - [=](BodyComponent& b) - { - b2BodyDef body_def = b2DefaultBodyDef(); - body_def.type = b2_kinematicBody; - body_def.position = physics->convert_to_meters(start); - body_def.userData = this; - b.id = b2CreateBody(physics->world, &body_def); - - b2SurfaceMaterial body_material = b2DefaultSurfaceMaterial(); - - b2ShapeDef circle_shape_def = b2DefaultShapeDef(); - circle_shape_def.density = 1.0f; - circle_shape_def.material = body_material; - circle_shape_def.isSensor = true; - circle_shape_def.enableSensorEvents = true; - - b2Circle circle_shape = {b2Vec2_zero, physics->convert_to_meters(radius)}; - b2CreateCircleShape(b.id, &circle_shape_def, &circle_shape); - }); - - animation = add_component(body); - if (type == Bat) - { - animation->add_animation("move", - std::vector{"assets/pixel_platformer/enemies/bat_1.png", - "assets/pixel_platformer/enemies/bat_2.png", - "assets/pixel_platformer/enemies/bat_3.png"}, - 5.0f); - } - else if (type == DrillHead) - { - animation->add_animation("move", - std::vector{"assets/pixel_platformer/enemies/drill_head_1.png", - "assets/pixel_platformer/enemies/drill_head_2.png"}, - 5.0f); - } - else if (type == BlockHead) - { - animation->add_animation("move", - std::vector{"assets/pixel_platformer/enemies/block_head_1.png", - "assets/pixel_platformer/enemies/block_head_2.png"}, - 5.0f); - } - animation->play("move"); - - GameObject::init_object(); - - // Start moving towards end position. - Vector2 to_end = end - body->get_position_pixels(); - to_end = Vector2Normalize(to_end); - body->set_velocity(to_end * 50.0f); - } - - void update(float delta_time) override - { - b2Circle circle_shape = {body->get_position_meters(), physics->convert_to_meters(radius * 2.0f)}; - if (b2PointInCircle(physics->convert_to_meters(end), &circle_shape)) - { - // Move towards start position. - Vector2 to_start = start - body->get_position_pixels(); - to_start = Vector2Normalize(to_start); - body->set_velocity(to_start * 50.0f); - } - else if (b2PointInCircle(physics->convert_to_meters(start), &circle_shape)) - { - // Move towards end position. - Vector2 to_end = end - body->get_position_pixels(); - to_end = Vector2Normalize(to_end); - body->set_velocity(to_end * 50.0f); - } - - // Check for collisions. - auto sensor_contacts = body->get_sensor_overlaps(); - for (auto contact_body_id : sensor_contacts) - { - auto user_data = static_cast(b2Body_GetUserData(contact_body_id)); - if (user_data && user_data->has_tag("character")) - { - // Hit player. - CollectingCharacter* character = static_cast(user_data); - character->die(); - } - } - - // Flip based on velocity. - Vector2 velocity = body->get_velocity_pixels(); - animation->flip_x = velocity.x > 0.0f; - } -}; - -/** - * A collectible coin. - */ -class Coin : public GameObject -{ -public: - Vector2 position; - PhysicsService* physics; - BodyComponent* body; - AnimationController* animation; - SoundComponent* collect_sound; - - Coin(Vector2 position) : position(position) {} - void init() override - { - physics = scene->get_service(); - - body = add_component( - [=](BodyComponent& b) - { - b2BodyDef body_def = b2DefaultBodyDef(); - body_def.type = b2_staticBody; - body_def.position = physics->convert_to_meters(position); - body_def.userData = this; - b.id = b2CreateBody(physics->world, &body_def); - - b2SurfaceMaterial body_material = b2DefaultSurfaceMaterial(); - - b2ShapeDef circle_shape_def = b2DefaultShapeDef(); - circle_shape_def.density = 1.0f; - circle_shape_def.material = body_material; - circle_shape_def.isSensor = true; - circle_shape_def.enableSensorEvents = true; - - b2Circle circle_shape = {b2Vec2_zero, physics->convert_to_meters(8.0f)}; - b2CreateCircleShape(b.id, &circle_shape_def, &circle_shape); - }); - - animation = add_component(body); - animation->add_animation("spin", - std::vector{"assets/pixel_platformer/items/coin_1.png", - "assets/pixel_platformer/items/coin_2.png"}, - 5.0f); - animation->play("spin"); - - collect_sound = add_component("assets/sounds/coin.wav"); - } - - void update(float delta_time) override - { - auto sensor_contacts = body->get_sensor_overlaps(); - for (auto contact_body_id : sensor_contacts) - { - auto user_data = static_cast(b2Body_GetUserData(contact_body_id)); - if (user_data && user_data->has_tag("character")) - { - // Collected by character. - collect_sound->play(); - - // Disable the coin. - is_active = false; - body->disable(); - - // Increase the score on the character. - CollectingCharacter* character = static_cast(user_data); - character->score += 1; - break; - } - } - } -}; - -/** - * A collecting game scene. - */ -class CollectingScene : public Scene -{ -public: - WindowManager* window_manager; - FontManager* font_manager; - std::vector> characters; - LevelService* level; - PhysicsService* physics; - std::vector> cameras; - Vector2 screen_size; - float scale = 2.5f; - - void init_services() override - { - // TextureService and SoundService are needed by other components and game objects. - add_service(); - add_service(); - - // PhysicsService is used by LevelService and must be added first. - physics = add_service(); - // Setup LDtk level. Checkout the file in LDtk editor to see how it's built. - std::vector collision_names = {"walls", "clouds", "trees"}; - level = add_service("assets/levels/collecting.ldtk", "Level", collision_names); - } - - void init() override - { - window_manager = game->get_manager(); - font_manager = game->get_manager(); - - const auto& entities_layer = level->get_layer_by_name("Entities"); - - // Create player characters at the "Start" entities. - auto player_entities = level->get_entities_by_name("Start"); - - for (int i = 0; i < player_entities.size() && i < 4; i++) - { - auto& player_entity = player_entities[i]; - CharacterParams params; - params.position = level->convert_to_pixels(player_entity->getPosition()); - params.width = 16; - params.height = 24; - auto character = add_game_object(params, i + 1); - character->add_tag("character"); - characters.push_back(character); - } - - // Create enemies at the each enemy entity. - auto bat_entities = level->get_entities_by_name("Bat"); - for (auto& bat_entity : bat_entities) - { - auto start_point = bat_entity->getPosition(); - Vector2 start_position = level->convert_to_pixels(start_point); - ldtk::IntPoint end_point = bat_entity->getField("end").value(); - // Annoyingly, Point fields in LDtk are in cell coordinates rather than pixel coordinates, and the cell - // size is dependent on the layer. - Vector2 end_position = level->convert_cells_to_pixels(end_point, entities_layer); - auto enemy = add_game_object(EnemyType::Bat, start_position, end_position); - enemy->add_tag("enemy"); - } - - auto drill_entities = level->get_entities_by_name("DrillHead"); - for (auto& drill_entity : drill_entities) - { - auto start_point = drill_entity->getPosition(); - Vector2 start_position = level->convert_to_pixels(start_point); - ldtk::IntPoint end_point = drill_entity->getField("end").value(); - Vector2 end_position = level->convert_cells_to_pixels(end_point, entities_layer); - auto enemy = add_game_object(EnemyType::DrillHead, start_position, end_position); - enemy->add_tag("enemy"); - } - - auto block_entities = level->get_entities_by_name("BlockHead"); - for (auto& block_entity : block_entities) - { - auto start_point = block_entity->getPosition(); - Vector2 start_position = level->convert_to_pixels(start_point); - ldtk::IntPoint end_point = block_entity->getField("end").value(); - Vector2 end_position = level->convert_cells_to_pixels(end_point, entities_layer); - auto enemy = add_game_object(EnemyType::BlockHead, start_position, end_position); - enemy->add_tag("enemy"); - } - - // Create coins at the "Coin" entities. - auto coin_entities = level->get_entities_by_name("Coin"); - for (auto& coin_entity : coin_entities) - { - Vector2 coin_position = level->convert_to_pixels(coin_entity->getPosition()); - auto coin = add_game_object(coin_position); - coin->add_tag("coin"); - } - - // Setup cameras. - screen_size = - Vector2{static_cast(window_manager->get_width()), static_cast(window_manager->get_height())}; - for (int i = 0; i < characters.size(); i++) - { - auto cam = add_game_object(screen_size / scale, level->get_size()); - cameras.push_back(cam); - } - } - - void update(float delta_time) override - { - // Set the camera target to follow each character. - for (int i = 0; i < cameras.size(); i++) - { - cameras[i]->target = characters[i]->body->get_position_pixels(); - } - - auto new_screen_size = Vector2{static_cast(GetScreenWidth()), static_cast(GetScreenHeight())}; - if (new_screen_size != screen_size) - { - // Window resized, update cameras. - screen_size = new_screen_size; - // Scale the cameras so they are zoomed in when drawn. - float screen_scale = window_manager->get_width() / screen_size.x; - for (auto camera : cameras) - { - camera->size = screen_size / scale * screen_scale; - camera->camera.offset = {camera->size.x / 2.0f, camera->size.y / 2.0f}; - UnloadRenderTexture(camera->renderer); - camera->renderer = LoadRenderTexture((int)camera->size.x, (int)camera->size.y); - } - } - - // Trigger scene change on Enter key or gamepad start button. - if (IsKeyPressed(KEY_ENTER) || IsGamepadButtonPressed(0, GAMEPAD_BUTTON_MIDDLE_RIGHT)) - { - game->go_to_scene_next(); - } - } - - /** - * We override draw_scene instead of draw to control when Scene::draw_scene is called. - * This allows us to draw the scene inside the camera's Begin/End block and render the camera to a texture. - */ - void draw_scene() override - { - // The scene needs to be rendered once per camera. - for (auto camera : cameras) - { - camera->draw_begin(); - Scene::draw_scene(); - // physics->draw_debug(); - // camera->draw_debug(); - camera->draw_end(); - } - - // Draw the cameras. - ClearBackground(MAGENTA); - - // Draw each camera's texture side by side. - for (int i = 0; i < cameras.size(); i++) - { - if (i == 0) - { - cameras[i]->draw_texture_pro(0, 0, screen_size.x / 2.0f, screen_size.y / 2.0f); - DrawTextEx(font_manager->get_font("Tiny5"), - TextFormat("Score: %d", characters[0]->score), - Vector2{20.0f, 20.0f}, - 40.0f, - 2.0f, - BLACK); - } - else if (i == 1) - { - cameras[i]->draw_texture_pro(screen_size.x / 2.0f, 0, screen_size.x / 2.0f, screen_size.y / 2.0f); - DrawTextEx(font_manager->get_font("Tiny5"), - TextFormat("Score: %d", characters[1]->score), - Vector2{screen_size.x / 2.0f + 20.0f, 20.0f}, - 40.0f, - 2.0f, - BLACK); - } - else if (i == 2) - { - cameras[i]->draw_texture_pro(0, screen_size.y / 2.0f, screen_size.x / 2.0f, screen_size.y / 2.0f); - DrawTextEx(font_manager->get_font("Tiny5"), - TextFormat("Score: %d", characters[2]->score), - Vector2{20.0f, screen_size.y / 2.0f + 20.0f}, - 40.0f, - 2.0f, - BLACK); - } - else if (i == 3) - { - cameras[i]->draw_texture_pro(screen_size.x / 2.0f, - screen_size.y / 2.0f, - screen_size.x / 2.0f, - screen_size.y / 2.0f); - DrawTextEx(font_manager->get_font("Tiny5"), - TextFormat("Score: %d", characters[3]->score), - Vector2{screen_size.x / 2.0f + 20.0f, screen_size.y / 2.0f + 20.0f}, - 40.0f, - 2.0f, - BLACK); - } - } - - // Draw split lines. - DrawLineEx(Vector2{screen_size.x / 2.0f, 0}, Vector2{screen_size.x / 2.0f, screen_size.y}, 4.0f, GRAY); - DrawLineEx(Vector2{0, screen_size.y / 2.0f}, Vector2{screen_size.x, screen_size.y / 2.0f}, 4.0f, GRAY); - } -}; diff --git a/src/samples/fighting_game.h b/src/samples/fighting_game.h deleted file mode 100644 index 53f11a8..0000000 --- a/src/samples/fighting_game.h +++ /dev/null @@ -1,564 +0,0 @@ -/** - * Demonstration of a shared camera, multiple characters, and basic fighting mechanics. - * Shows how to setup a level with the LevelService and physics bodies with the PhysicsService. - * Also shows how to use animations and sounds. - */ - -#pragma once - -#include "engine/prefabs/includes.h" - -/** - * A basic fighting character. - * See also engine/prefabs/game_objects.h for PlatformerCharacter. - */ -class FightingCharacter : public GameObject -{ -public: - CharacterParams p; - PhysicsService* physics; - LevelService* level; - BodyComponent* body; - PlatformerMovementComponent* movement; - AnimationController* animation; - MultiComponent* sounds; - SoundComponent* jump_sound; - SoundComponent* hit_sound; - SoundComponent* die_sound; - - bool grounded = false; - bool on_wall_left = false; - bool on_wall_right = false; - float coyote_timer = 0.0f; - float jump_buffer_timer = 0.0f; - int gamepad = 0; - int player_number = 1; - float width = 24.0f; - float height = 40.0f; - bool fall_through = false; - float fall_through_timer = 0.0f; - float fall_through_duration = 0.2f; - float attack_display_timer = 0.0f; - float attack_display_duration = 0.1f; - bool attack = false; - - FightingCharacter(CharacterParams p, int player_number = 1) : - p(p), - player_number(player_number), - gamepad(player_number - 1), - width(p.width), - height(p.height) - { - } - - void init() override - { - - // Grab the physics service. - // All get_service calls should be done in init(). get_service is not quick and this also allows us to test - // that all services exist during init time. - physics = scene->get_service(); - - // Setup the character's physics body using the BodyComponent initialization callback lambda. - body = add_component( - [=](BodyComponent& b) - { - b2BodyDef body_def = b2DefaultBodyDef(); - body_def.type = b2_dynamicBody; - body_def.fixedRotation = true; - body_def.isBullet = true; - // All units in box2d are in meters. - body_def.position = physics->convert_to_meters(p.position); - b.id = b2CreateBody(physics->world, &body_def); - - b2SurfaceMaterial body_material = b2DefaultSurfaceMaterial(); - body_material.friction = p.friction; - body_material.restitution = p.restitution; - - b2ShapeDef box_shape_def = b2DefaultShapeDef(); - box_shape_def.density = p.density; - box_shape_def.material = body_material; - - // Needed to presolve one-way behavior. - box_shape_def.enablePreSolveEvents = true; - - // We use a rounded box which helps with getting stuck on edges. - b2Polygon body_polygon = b2MakeRoundedBox(physics->convert_to_meters(p.width / 2.0f), - physics->convert_to_meters(p.height / 2.0f), - physics->convert_to_meters(0.25)); - b2CreatePolygonShape(b.id, &box_shape_def, &body_polygon); - }); - - PlatformerMovementParams mp; - mp.width = p.width; - mp.height = p.height; - movement = add_component(mp); - - level = scene->get_service(); - - // TODO: Is only allowing one component per type really as cool an idea as I thought? - sounds = add_component>(); - jump_sound = sounds->add_component("jump", "assets/sounds/jump.wav"); - hit_sound = sounds->add_component("hit", "assets/sounds/hit.wav"); - die_sound = sounds->add_component("die", "assets/sounds/die.wav"); - - // Setup animations. - animation = add_component(body); - if (player_number == 1) - { - animation->add_animation("run", - std::vector{"assets/sunnyland/fox/run-1.png", - "assets/sunnyland/fox/run-2.png", - "assets/sunnyland/fox/run-3.png", - "assets/sunnyland/fox/run-4.png", - "assets/sunnyland/fox/run-5.png", - "assets/sunnyland/fox/run-6.png"}, - 10.0f); - animation->add_animation("idle", - std::vector{"assets/sunnyland/fox/idle-1.png", - "assets/sunnyland/fox/idle-2.png", - "assets/sunnyland/fox/idle-3.png", - "assets/sunnyland/fox/idle-4.png"}, - 5.0f); - animation->add_animation("jump", std::vector{"assets/sunnyland/fox/jump-1.png"}, 0.0f); - animation->add_animation("fall", std::vector{"assets/sunnyland/fox/jump-2.png"}, 0.0f); - - animation->origin.y += 4; - } - else if (player_number == 2) - { - animation->add_animation("run", - std::vector{"assets/sunnyland/bunny/run-1.png", - "assets/sunnyland/bunny/run-2.png", - "assets/sunnyland/bunny/run-3.png", - "assets/sunnyland/bunny/run-4.png", - "assets/sunnyland/bunny/run-5.png", - "assets/sunnyland/bunny/run-6.png"}, - 10.0f); - animation->add_animation("idle", - std::vector{"assets/sunnyland/bunny/idle-1.png", - "assets/sunnyland/bunny/idle-2.png", - "assets/sunnyland/bunny/idle-3.png", - "assets/sunnyland/bunny/idle-4.png"}, - 10.0f); - animation->add_animation("jump", std::vector{"assets/sunnyland/bunny/jump-1.png"}, 0.0f); - animation->add_animation("fall", std::vector{"assets/sunnyland/bunny/jump-2.png"}, 0.0f); - - animation->origin.y += 8; - } - else if (player_number == 3) - { - animation->add_animation("run", - std::vector{"assets/sunnyland/squirrel/run-1.png", - "assets/sunnyland/squirrel/run-2.png", - "assets/sunnyland/squirrel/run-3.png", - "assets/sunnyland/squirrel/run-4.png", - "assets/sunnyland/squirrel/run-5.png", - "assets/sunnyland/squirrel/run-6.png"}, - 10.0f); - animation->add_animation("idle", - std::vector{"assets/sunnyland/squirrel/idle-1.png", - "assets/sunnyland/squirrel/idle-2.png", - "assets/sunnyland/squirrel/idle-3.png", - "assets/sunnyland/squirrel/idle-4.png", - "assets/sunnyland/squirrel/idle-5.png", - "assets/sunnyland/squirrel/idle-6.png", - "assets/sunnyland/squirrel/idle-7.png", - "assets/sunnyland/squirrel/idle-8.png"}, - 8.0f); - animation->add_animation("jump", - std::vector{"assets/sunnyland/squirrel/jump-1.png", - "assets/sunnyland/squirrel/jump-2.png", - "assets/sunnyland/squirrel/jump-3.png", - "assets/sunnyland/squirrel/jump-4.png"}, - 15.0f); - animation->origin.y += 7; - } - else if (player_number == 4) - { - animation->add_animation("run", - std::vector{"assets/sunnyland/imp/run-1.png", - "assets/sunnyland/imp/run-2.png", - "assets/sunnyland/imp/run-3.png", - "assets/sunnyland/imp/run-4.png", - "assets/sunnyland/imp/run-5.png", - "assets/sunnyland/imp/run-6.png", - "assets/sunnyland/imp/run-7.png", - "assets/sunnyland/imp/run-8.png"}, - 10.0f); - animation->add_animation("idle", - std::vector{"assets/sunnyland/imp/idle-1.png", - "assets/sunnyland/imp/idle-2.png", - "assets/sunnyland/imp/idle-3.png", - "assets/sunnyland/imp/idle-4.png"}, - 10.0f); - animation->add_animation("jump", std::vector{"assets/sunnyland/imp/jump-1.png"}, 0.0f); - animation->add_animation("fall", std::vector{"assets/sunnyland/imp/jump-4.png"}, 0.0f); - animation->origin.y += 10; - } - } - - void update(float delta_time) override - { - // Get input and route to movement component. - float deadzone = 0.1f; - - const bool jump_pressed = - IsKeyPressed(KEY_W) || IsGamepadButtonPressed(gamepad, GAMEPAD_BUTTON_RIGHT_FACE_DOWN); - const bool jump_held = IsKeyDown(KEY_W) || IsGamepadButtonDown(gamepad, GAMEPAD_BUTTON_RIGHT_FACE_DOWN); - - float move_x = 0.0f; - move_x = GetGamepadAxisMovement(gamepad, GAMEPAD_AXIS_LEFT_X); - if (fabsf(move_x) < deadzone) - { - move_x = 0.0f; - } - if (IsKeyDown(KEY_D) || IsGamepadButtonDown(gamepad, GAMEPAD_BUTTON_LEFT_FACE_RIGHT)) - { - move_x = 1.0f; - } - else if (IsKeyDown(KEY_A) || IsGamepadButtonDown(gamepad, GAMEPAD_BUTTON_LEFT_FACE_LEFT)) - { - move_x = -1.0f; - } - - movement->set_input(move_x, jump_pressed, jump_held); - - if (movement->grounded && jump_pressed) - { - jump_sound->play(); - } - - if (fabsf(movement->move_x) > 0.1f) - { - animation->play("run"); - animation->flip_x = movement->move_x < 0.0f; - } - else - { - animation->play("idle"); - } - - if (!movement->grounded) - { - // Squirrel doesn't have a fall animation so we do this monstrosity. - if (player_number != 3) - { - if (body->get_velocity_meters().y < 0.0f) - { - animation->play("jump"); - } - else - { - - animation->play("fall"); - } - } - else - { - animation->play("jump"); - } - } - - // Custom one-way platform fall-through logic. - float move_y = GetGamepadAxisMovement(gamepad, GAMEPAD_AXIS_LEFT_Y); - if (IsKeyPressed(KEY_S) || IsGamepadButtonPressed(gamepad, GAMEPAD_BUTTON_LEFT_FACE_DOWN) || move_y > 0.5f) - { - fall_through = true; - fall_through_timer = fall_through_duration; - } - - if (fall_through_timer > 0.0f) - { - fall_through_timer = std::max(0.0f, fall_through_timer - delta_time); - if (fall_through_timer == 0.0f) - { - fall_through = false; - } - } - - // Attack logic. - if (IsKeyPressed(KEY_SPACE) || IsGamepadButtonPressed(gamepad, GAMEPAD_BUTTON_RIGHT_FACE_RIGHT)) - { - attack = true; - attack_display_timer = attack_display_duration; - Vector2 position = body->get_position_pixels(); - position.x += (width / 2.0f + 8.0f) * (animation->flip_x ? -1.0f : 1.0f); - auto bodies = physics->circle_overlap(position, 8.0f, body->id); - for (auto& other_body : bodies) - { - // Apply impulse to other character. - b2Vec2 impulse = b2Vec2{animation->flip_x ? -10.0f : 10.0f, -10.0f}; - b2Body_ApplyLinearImpulse(other_body, impulse, b2Body_GetPosition(other_body), true); - hit_sound->play(); - } - } - - if (attack_display_timer > 0.0f) - { - attack_display_timer = std::max(0.0f, attack_display_timer - delta_time); - if (attack_display_timer == 0.0f) - { - attack = false; - } - } - - // Death and respawn logic. - if (body->get_position_pixels().y > level->get_size().y + 200.0f) - { - // Re-spawn at start position. - body->set_position(p.position); - body->set_velocity(Vector2{0.0f, 0.0f}); - die_sound->play(); - } - } - - void draw() override - { - // Draw attack indicator. - if (attack) - { - Vector2 position = body->get_position_pixels(); - position.x += (width / 2.0f + 8.0f) * (animation->flip_x ? -1.0f : 1.0f); - DrawCircleV(position, 8.0f, Fade(RED, 0.5f)); - } - // Animations are drawn by the AnimationController component. - } - - /** - * Pre-solve callback for one-way platforms. - * - * @param body_a The first body in the collision. - * @param body_b The second body in the collision. - * @param manifold The contact manifold. - * @param platforms The list of one-way platform StaticBox objects. - * @return true to enable the contact, false to disable it. - */ - bool PreSolve(b2BodyId body_a, - b2BodyId body_b, - b2Manifold* manifold, - std::vector> platforms) const - { - float sign = 0.0f; - b2BodyId other = b2_nullBodyId; - - // Check which body is the character. - if (body_a == body->id) - { - sign = 1.0f; - other = body_b; - } - else if (body_b == body->id) - { - sign = -1.0f; - other = body_a; - } - - if (sign * manifold->normal.y < 0.5f) - { - // Normal points down, disable contact. - return false; - } - - if (fall_through) - { - for (auto& platform : platforms) - { - if (other == platform->body) - { - // Character is in fall-through state, disable contact. - return false; - } - } - } - - // Otherwise, enable contact. - return true; - } -}; - -/** - * A fighting game scene. - */ -class FightingScene : public Scene -{ -public: - RenderTexture2D renderer; - Rectangle render_rect; - std::vector> platforms; - std::vector> characters; - LevelService* level; - PhysicsService* physics; - std::shared_ptr camera; - - void init_services() override - { - // TextureService and SoundService are needed by other components and game objects. - add_service(); - add_service(); - - // PhysicsService is used by LevelService and must be added first. - physics = add_service(); - // Setup LDtk level. Checkout the file in LDtk editor to see how it's built. - std::vector collision_names = {"walls"}; - level = add_service("assets/levels/fighting.ldtk", "Stage", collision_names); - } - - void init() override - { - auto window_manager = game->get_manager(); - - // Find one-way platform entities in the level and create StaticBox game objects for them. - auto platform_entities = level->get_entities_by_name("One_way_platform"); - for (auto& platform_entity : platform_entities) - { - Vector2 position = level->convert_to_pixels(platform_entity->getPosition()); - Vector2 size = level->convert_to_pixels(platform_entity->getSize()); - auto platform = add_game_object(position + size / 2.0f, size); - platform->is_visible = false; - platform->add_tag("platform"); - platforms.push_back(platform); - } - - // Pre-solve callback to handle one-way platforms. - b2World_SetPreSolveCallback(physics->world, PreSolveStatic, this); - - // Create player characters at the "Start" entities. - auto player_entities = level->get_entities_by_name("Start"); - - for (int i = 0; i < player_entities.size() && i < 4; i++) - { - auto& player_entity = player_entities[i]; - CharacterParams params; - params.position = level->convert_to_pixels(player_entity->getPosition()); - params.width = 16; - params.height = 24; - auto character = add_game_object(params, i + 1); - character->add_tag("character"); - characters.push_back(character); - } - - // Setup shared camera. - camera = add_game_object(level->get_size(), - Vector2{0.0f, 0.0f}, - Vector2{300.0f, 300.0f}, - 0.0f, - 0.0f, - 0.0f, - 0.0f); - camera->target = level->get_size() / 2.0f; - - // Disable the background layer from drawing. We'll draw it manually in draw_scene(). - level->set_layer_visibility("Background", false); - - renderer = LoadRenderTexture((int)level->get_size().x, (int)level->get_size().y); - } - - void update(float delta_time) override - { - // Set the camera target to the center of all players. - // Also zoom to fit all players. - Vector2 avg_position = {0.0f, 0.0f}; - Vector2 min_point = {FLT_MAX, FLT_MAX}; - Vector2 max_point = {-FLT_MAX, -FLT_MAX}; - for (auto& character : characters) - { - Vector2 char_pos = character->body->get_position_pixels(); - avg_position = avg_position + char_pos; - min_point.x = std::min(min_point.x, char_pos.x); - min_point.y = std::min(min_point.y, char_pos.y); - max_point.x = std::max(max_point.x, char_pos.x); - max_point.y = std::max(max_point.y, char_pos.y); - } - avg_position = avg_position / static_cast(characters.size()); - camera->target = avg_position; - - // Force camera to align with pixel grid to avoid sub-pixel jitter. - camera->target.x = floorf(camera->target.x); - camera->target.y = floorf(camera->target.y); - - // Calculate zoom to fit all characters. - float distance = sqrtf((max_point.x - min_point.x) * (max_point.x - min_point.x) + - (max_point.y - min_point.y) * (max_point.y - min_point.y)); - float level_diagonal = - sqrtf(level->get_size().x * level->get_size().x + level->get_size().y * level->get_size().y); - float zoom = level_diagonal / (distance + 400); - zoom = std::clamp(zoom, 0.5f, 2.0f); - - // Lerp zoom for smoothness. - // Note that this style of camera zoom is incompatible with pixel perfect rendering. Remove this line to see - // pixel perfect scaling. - camera->camera.zoom += (zoom - camera->camera.zoom) * std::min(1.0f, delta_time * 5.0f); - - // Draw the level centered in the window. - float aspect_ratio = level->get_size().x / level->get_size().y; - float render_scale = GetScreenHeight() / level->get_size().y; - Vector2 render_size = {level->get_size().y * render_scale * aspect_ratio, level->get_size().y * render_scale}; - auto pos = (Vector2{(float)GetScreenWidth(), (float)GetScreenHeight()} - render_size) / 2.0f; - render_rect = Rectangle{pos.x, pos.y, render_size.x, render_size.y}; - - // Trigger scene change on Enter key or gamepad start button. - if (IsKeyPressed(KEY_ENTER) || IsGamepadButtonPressed(0, GAMEPAD_BUTTON_MIDDLE_RIGHT)) - { - game->go_to_scene_next(); - } - } - - /** - * We override draw_scene instead of draw to control when Scene::draw_scene is called. - * This allows us to draw the scene inside the camera's Begin/End block and render the camera to a texture. - */ - void draw_scene() override - { - // Draw to render texture. - BeginTextureMode(renderer); - ClearBackground(MAGENTA); - - // Draw the background layer outside of the camera. - level->draw_layer("Background"); - - // Start the camera and render the scene inside. - camera->draw_begin(); - Scene::draw_scene(); - // physics->draw_debug(); - // camera->draw_debug(); - camera->draw_end(); - - EndTextureMode(); - - // Draw centered. - DrawTexturePro( - renderer.texture, - Rectangle{0, 0, static_cast(renderer.texture.width), -static_cast(renderer.texture.height)}, - render_rect, - Vector2{0.0f, 0.0f}, - 0.0f, - WHITE); - } - - /** - * Static pre-solve callback to route to the appropriate character. - * - * @param shape_a The first shape in the collision. - * @param shape_b The second shape in the collision. - * @param manifold The contact manifold. - * @param context The FightingScene instance. - * @return true to enable the contact, false to disable it. - */ - static bool PreSolveStatic(b2ShapeId shape_a, b2ShapeId shape_b, b2Manifold* manifold, void* context) - { - FightingScene* self = static_cast(context); - b2BodyId body_a = b2Shape_GetBody(shape_a); - b2BodyId body_b = b2Shape_GetBody(shape_b); - - // Find which character is involved and route to its PreSolve method. - for (auto& character : self->characters) - { - if (body_a == character->body->id || body_b == character->body->id) - { - return character->PreSolve(body_a, body_b, manifold, self->platforms); - } - } - return true; - } -}; diff --git a/src/samples/ghhb_game.h b/src/samples/ghhb_game.h new file mode 100644 index 0000000..ae8aafd --- /dev/null +++ b/src/samples/ghhb_game.h @@ -0,0 +1,379 @@ +/** + * DDR / Guitar Hero style rhythm game: 12 lanes, pre-coded notes, controller support. + * Controller: D-pad (lanes 0-3), face X/Y/A/B (4-7), LB/LT/RB/RT (8-11). 8bitdo compatible. + */ + +#pragma once + +#include "engine/prefabs/includes.h" +#include +#include +#include + +namespace +{ +constexpr int LANE_COUNT = 12; +constexpr float HIT_WINDOW_PX = 80.0f; +constexpr float RECEPTOR_HEIGHT = 100.0f; +constexpr float SCROLL_PX_PER_SEC = 350.0f; + +const int GAMEPAD_BUTTONS[LANE_COUNT] = { + GAMEPAD_BUTTON_LEFT_FACE_LEFT, + GAMEPAD_BUTTON_LEFT_FACE_UP, + GAMEPAD_BUTTON_LEFT_FACE_RIGHT, + GAMEPAD_BUTTON_LEFT_FACE_DOWN, + GAMEPAD_BUTTON_RIGHT_FACE_LEFT, + GAMEPAD_BUTTON_RIGHT_FACE_UP, + GAMEPAD_BUTTON_RIGHT_FACE_RIGHT, + GAMEPAD_BUTTON_RIGHT_FACE_DOWN, + GAMEPAD_BUTTON_LEFT_TRIGGER_2, + GAMEPAD_BUTTON_LEFT_TRIGGER_1, + GAMEPAD_BUTTON_RIGHT_TRIGGER_1, + GAMEPAD_BUTTON_RIGHT_TRIGGER_2, +}; + +const int KEY_KEYS[LANE_COUNT] = { + KEY_Q, + KEY_W, + KEY_E, + KEY_R, + KEY_A, + KEY_S, + KEY_D, + KEY_F, + KEY_Z, + KEY_X, + KEY_C, + KEY_V, +}; + +struct Note +{ + float time = 0.0f; + int lane = 0; + + float y_position(float song_time, float hit_line_y) const + { + return hit_line_y - (time - song_time) * SCROLL_PX_PER_SEC; + } +}; + +std::vector default_chart() +{ + std::vector notes; + float t = 2.0f; + for (int i = 0; i < 4; i++) + { + for (int lane = 0; lane < LANE_COUNT; lane++) + { + notes.push_back(Note{t, lane}); + t += 0.4f; + } + t += 0.6f; + } + for (int lane = 0; lane < LANE_COUNT; lane++) + { + notes.push_back(Note{t, lane}); + t += 0.2f; + } + t += 0.5f; + for (int i = 0; i < 3; i++) + { + for (int lane = 0; lane < LANE_COUNT; lane++) + { + notes.push_back(Note{t + lane * 0.08f, lane}); + } + t += 0.8f; + } + return notes; +} +} // namespace + +class GHHBScene : public Scene +{ +public: + Font font = {0}; + std::vector chart = default_chart(); + std::vector spawned; + std::unordered_set completed_notes; + float song_time = 0.0f; + int score = 0; + int combo = 0; + float hit_line_y = 0.0f; + float lane_width = 0.0f; + float screen_width = 0.0f; + float screen_height = 0.0f; + int gamepad_id = 0; + static constexpr float PRESS_FLASH_DURATION = 0.12f; + static constexpr float MISS_FLASH_DURATION = 0.15f; + float press_flash_timer[LANE_COUNT] = {0}; + float hit_flash_timer[LANE_COUNT] = {0}; + float miss_flash_timer[LANE_COUNT] = {0}; + bool game_ended = false; + static constexpr float RESULTS_DELAY_AFTER_LAST_NOTE = 1.0f; + + void on_enter() override + { + song_time = 0.0f; + score = 0; + combo = 0; + game_ended = false; + spawned.clear(); + completed_notes.clear(); + for (int i = 0; i < LANE_COUNT; i++) + { + press_flash_timer[i] = 0.0f; + hit_flash_timer[i] = 0.0f; + miss_flash_timer[i] = 0.0f; + } + } + + void init() override + { + auto font_manager = game->get_manager(); + font = font_manager->get_font("Roboto"); + screen_width = static_cast(GetScreenWidth()); + screen_height = static_cast(GetScreenHeight()); + hit_line_y = screen_height - RECEPTOR_HEIGHT / 2.0f; + lane_width = screen_width / LANE_COUNT; + } + + float lane_center_x(int lane) const + { + return (lane + 0.5f) * lane_width; + } + + bool is_note_hittable(const Note& n) const + { + float y = n.y_position(song_time, hit_line_y); + return fabsf(y - hit_line_y) <= HIT_WINDOW_PX; + } + + void consume_note(Note* n) + { + auto it = std::find_if(spawned.begin(), spawned.end(), [n](Note* p) { return p == n; }); + if (it != spawned.end()) + { + hit_flash_timer[n->lane] = PRESS_FLASH_DURATION; + spawned.erase(it); + completed_notes.insert(n); + } + combo++; + score += 100 + std::min(combo * 10, 50); + } + + void update(float delta_time) override + { + if (game_ended) + { + if (IsKeyPressed(KEY_ENTER) || IsGamepadButtonPressed(0, GAMEPAD_BUTTON_MIDDLE_RIGHT)) + { + game->go_to_scene("title"); + } + return; + } + + song_time += delta_time; + + float last_note_time = 0.0f; + for (const auto& n : chart) + { + if (n.time > last_note_time) + { + last_note_time = n.time; + } + } + float note_exit_seconds = HIT_WINDOW_PX / SCROLL_PX_PER_SEC; + if (song_time >= last_note_time + note_exit_seconds + RESULTS_DELAY_AFTER_LAST_NOTE) + { + game_ended = true; + return; + } + + float lead_seconds = (hit_line_y - 60.0f) / SCROLL_PX_PER_SEC; + for (auto& n : chart) + { + if (completed_notes.count(&n) != 0) + { + continue; + } + bool already = std::find_if(spawned.begin(), spawned.end(), + [&n](Note* p) { return p == &n; }) != spawned.end(); + if (!already && song_time >= n.time - lead_seconds) + { + spawned.push_back(&n); + } + } + + for (auto it = spawned.begin(); it != spawned.end();) + { + Note* n = *it; + float y = n->y_position(song_time, hit_line_y); + if (y > hit_line_y) + { + miss_flash_timer[n->lane] = MISS_FLASH_DURATION; + completed_notes.insert(n); + it = spawned.erase(it); + combo = 0; + } + else + { + ++it; + } + } + + for (int lane = 0; lane < LANE_COUNT; lane++) + { + press_flash_timer[lane] -= delta_time; + if (press_flash_timer[lane] < 0.0f) + { + press_flash_timer[lane] = 0.0f; + } + hit_flash_timer[lane] -= delta_time; + if (hit_flash_timer[lane] < 0.0f) + { + hit_flash_timer[lane] = 0.0f; + } + miss_flash_timer[lane] -= delta_time; + if (miss_flash_timer[lane] < 0.0f) + { + miss_flash_timer[lane] = 0.0f; + } + bool pressed = IsKeyPressed(KEY_KEYS[lane]) || + IsGamepadButtonPressed(gamepad_id, GAMEPAD_BUTTONS[lane]); + if (pressed) + { + press_flash_timer[lane] = PRESS_FLASH_DURATION; + } + if (!pressed) + { + continue; + } + Note* best = nullptr; + float best_dist = 1e9f; + for (Note* n : spawned) + { + if (n->lane != lane) + { + continue; + } + if (!is_note_hittable(*n)) + { + continue; + } + float y = n->y_position(song_time, hit_line_y); + float d = fabsf(y - hit_line_y); + if (d < best_dist) + { + best_dist = d; + best = n; + } + } + if (best != nullptr) + { + consume_note(best); + } + } + + if (IsKeyPressed(KEY_ENTER) || IsGamepadButtonPressed(0, GAMEPAD_BUTTON_MIDDLE_RIGHT)) + { + game->go_to_scene("title"); + } + } + + void draw() override + { + ClearBackground(Color{30, 30, 46, 255}); + + for (int lane = 0; lane < LANE_COUNT; lane++) + { + float cx = lane_center_x(lane); + DrawRectangle(static_cast(lane * lane_width), + 0, + static_cast(lane_width), + static_cast(screen_height), + lane % 2 == 0 ? Color{50, 50, 70, 255} : Color{45, 45, 65, 255}); + DrawLineEx(Vector2{cx, 0}, Vector2{cx, screen_height}, 2.0f, Color{70, 70, 90, 255}); + } + + float receptor_top = screen_height - RECEPTOR_HEIGHT; + DrawRectangle(0, + static_cast(receptor_top), + static_cast(screen_width), + static_cast(RECEPTOR_HEIGHT), + Color{60, 60, 100, 255}); + for (int lane = 0; lane < LANE_COUNT; lane++) + { + if (hit_flash_timer[lane] > 0.0f) + { + float alpha = 180.0f * (hit_flash_timer[lane] / PRESS_FLASH_DURATION); + DrawRectangle(static_cast(lane * lane_width), + static_cast(receptor_top), + static_cast(lane_width), + static_cast(RECEPTOR_HEIGHT), + Color{80, 255, 120, static_cast(alpha)}); + } + else if (press_flash_timer[lane] > 0.0f) + { + float alpha = 180.0f * (press_flash_timer[lane] / PRESS_FLASH_DURATION); + DrawRectangle(static_cast(lane * lane_width), + static_cast(receptor_top), + static_cast(lane_width), + static_cast(RECEPTOR_HEIGHT), + Color{255, 255, 255, static_cast(alpha)}); + } + if (miss_flash_timer[lane] > 0.0f) + { + float alpha = 200.0f * (miss_flash_timer[lane] / MISS_FLASH_DURATION); + DrawRectangle(static_cast(lane * lane_width), + static_cast(receptor_top), + static_cast(lane_width), + static_cast(RECEPTOR_HEIGHT), + Color{255, 80, 80, static_cast(alpha)}); + } + } + DrawLineEx(Vector2{0, hit_line_y}, Vector2{screen_width, hit_line_y}, 3.0f, WHITE); + + for (Note* n : spawned) + { + float y = n->y_position(song_time, hit_line_y); + if (y < -40.0f || y > screen_height + 40.0f) + { + continue; + } + float cx = lane_center_x(n->lane); + DrawCircle(static_cast(cx), static_cast(y), 22, Color{220, 100, 100, 255}); + DrawCircleLines(static_cast(cx), static_cast(y), 22, + Color{255, 150, 150, 255}); + } + + std::string score_text = "Score: " + std::to_string(score) + " Combo: " + std::to_string(combo); + DrawTextEx(font, score_text.c_str(), {20, 16}, 28, 1, WHITE); + DrawTextEx(font, "Arrows / D-pad / X Y A B / LB LT RB RT: hit when note reaches white line", + {20, receptor_top - 28}, 18, 1, Color{200, 200, 200, 255}); + + if (game_ended) + { + DrawRectangle(0, 0, static_cast(screen_width), static_cast(screen_height), + Color{0, 0, 0, 200}); + const char* title_text = "Song complete!"; + const float title_font_size = 42.0f; + float title_w = MeasureTextEx(font, title_text, title_font_size, 1).x; + DrawTextEx(font, title_text, + {screen_width / 2.0f - title_w / 2.0f, screen_height / 2.0f - 80}, + title_font_size, 1, WHITE); + std::string final_score_text = "Final score: " + std::to_string(score); + const float score_font_size = 32.0f; + float score_w = MeasureTextEx(font, final_score_text.c_str(), score_font_size, 1).x; + DrawTextEx(font, final_score_text.c_str(), + {screen_width / 2.0f - score_w / 2.0f, screen_height / 2.0f - 20}, + score_font_size, 1, Color{220, 220, 255, 255}); + const char* prompt = "Press Enter to return to menu"; + const float prompt_font_size = 20.0f; + float prompt_w = MeasureTextEx(font, prompt, prompt_font_size, 1).x; + DrawTextEx(font, prompt, + {screen_width / 2.0f - prompt_w / 2.0f, screen_height / 2.0f + 40}, + prompt_font_size, 1, Color{180, 180, 200, 255}); + } + } +}; diff --git a/src/samples/title_screen.h b/src/samples/title_screen.h index 846bb44..f14771b 100644 --- a/src/samples/title_screen.h +++ b/src/samples/title_screen.h @@ -6,7 +6,7 @@ class TitleScreen : public Scene { public: Font font; - std::string title = "Game Jam Kit"; + std::string title = "Guitar Hero But Better"; void init() override { auto font_manager = game->get_manager(); @@ -18,7 +18,7 @@ public: // Trigger scene change on Enter key or gamepad start button. if (IsKeyPressed(KEY_ENTER) || IsGamepadButtonPressed(0, GAMEPAD_BUTTON_MIDDLE_RIGHT)) { - game->go_to_scene_next(); + game->go_to_scene("ghhb"); } } @@ -28,7 +28,7 @@ public: auto height = GetScreenHeight(); auto title_text_size = MeasureTextEx(font, title.c_str(), 64, 0); - std::string subtitle = "Press Start or Enter to Switch Scenes"; + std::string subtitle = "Press Start or Enter to Play"; auto subtitle_text_size = MeasureTextEx(font, subtitle.c_str(), 32, 0); ClearBackground(SKYBLUE); diff --git a/src/samples/zombie_game.h b/src/samples/zombie_game.h deleted file mode 100644 index 055a2ed..0000000 --- a/src/samples/zombie_game.h +++ /dev/null @@ -1,551 +0,0 @@ -/** - * Demonstration of a top down shooter game. - * Shows how to draw lights using custom blend modes. - */ - -#pragma once - -#include "engine/prefabs/includes.h" -#include "rlgl.h" - -// Custom Blend Modes for lights. See https://www.raylib.com/examples/shapes/loader.html?name=shapes_top_down_lights -#define RLGL_SRC_ALPHA 0x0302 -#define RLGL_MIN 0x8007 - -class ZombieScene; - -/** - * A bullet fired by a character. - */ -class Bullet : public GameObject -{ -public: - PhysicsService* physics; - BodyComponent* body; - SpriteComponent* sprite; - SoundComponent* hit_sound; - float speed = 800.0f; // pixels per second - - void init() override - { - physics = scene->get_service(); - - body = add_component( - [=](BodyComponent& b) - { - b2BodyDef body_def = b2DefaultBodyDef(); - body_def.type = b2_dynamicBody; - body_def.isBullet = true; - // Start off-screen. - body_def.position = physics->convert_to_meters(Vector2{-1000.0f, -1000.0f}); - body_def.userData = this; - b.id = b2CreateBody(physics->world, &body_def); - - b2SurfaceMaterial body_material = b2DefaultSurfaceMaterial(); - body_material.restitution = 0.0f; - body_material.friction = 0.0f; - - b2ShapeDef circle_shape_def = b2DefaultShapeDef(); - circle_shape_def.density = 0.25f; - circle_shape_def.material = body_material; - - b2Circle circle_shape = {b2Vec2_zero, physics->convert_to_meters(8.0f)}; - b2CreateCircleShape(b.id, &circle_shape_def, &circle_shape); - }); - - sprite = add_component("assets/zombie_shooter/bullet.png", body); - - hit_sound = add_component("assets/sounds/hit.wav"); - } - - void update(float delta_time) override - { - auto contacts = body->get_contacts(); - for (const auto& contact : contacts) - { - // Deactivate the bullet if we hit anything. - is_active = false; - // Move it off-screen. - body->set_position(Vector2{-1000.0f, -1000.0f}); - body->set_velocity(Vector2{0.0f, 0.0f}); - - GameObject* other = static_cast(b2Body_GetUserData(contact)); - if (other) - { - if (other->has_tag("zombie")) - { - hit_sound->play(); - - // Hit a zombie, deactivate it too. - other->is_active = false; - // Move it off-screen. - auto zombie_body = other->get_component(); - if (zombie_body) - { - zombie_body->set_position(Vector2{-1000.0f, -1000.0f}); - zombie_body->set_velocity(Vector2{0.0f, 0.0f}); - zombie_body->disable(); - } - auto zombie_sprite = other->get_component(); - if (zombie_sprite) - { - zombie_sprite->set_position(Vector2{-1000.0f, -1000.0f}); - } - } - break; - } - } - } -}; - -/** - * A top-down character controlled by the player. - */ -class TopDownCharacter : public GameObject -{ -public: - Vector2 position = {0, 0}; - BodyComponent* body; - PhysicsService* physics; - SpriteComponent* sprite; - TopDownMovementComponent* movement; - MultiComponent* sounds; - SoundComponent* shoot_sound; - std::vector> bullets; - int player_num = 0; - int health = 10; - float contact_timer = 1.0f; - float contact_cooldown = 0.3f; - - TopDownCharacter(Vector2 position, std::vector> bullets, int player_num = 0) : - position(position), - bullets(std::move(bullets)), - player_num(player_num) - { - } - - void init() override - { - // Grab the physics service. - // All get_service calls should be done in init(). get_service is not quick and this also allows us to test - // that all services exist during init time. - physics = scene->get_service(); - - body = add_component( - [=](BodyComponent& b) - { - b2BodyDef body_def = b2DefaultBodyDef(); - body_def.type = b2_dynamicBody; - body_def.fixedRotation = true; - body_def.position = physics->convert_to_meters(position); - body_def.userData = this; - b.id = b2CreateBody(physics->world, &body_def); - - b2SurfaceMaterial body_material = b2DefaultSurfaceMaterial(); - - b2ShapeDef circle_shape_def = b2DefaultShapeDef(); - circle_shape_def.density = 1.0f; - circle_shape_def.material = body_material; - - b2Circle circle_shape = {b2Vec2_zero, physics->convert_to_meters(16.0f)}; - b2CreateCircleShape(b.id, &circle_shape_def, &circle_shape); - }); - - // Setup movement. - TopDownMovementParams mp; - mp.accel = 5000.0f; - mp.friction = 5000.0f; - mp.max_speed = 350.0f; - movement = add_component(mp); - - // Setup sounds. - // TODO: Is only allowing one component per type really as cool an idea as I thought? - sounds = add_component>(); - shoot_sound = sounds->add_component("shoot", "assets/sounds/shoot.wav"); - - // Setup sprite. - sprite = - add_component("assets/zombie_shooter/player_" + std::to_string(player_num + 1) + ".png"); - } - - void update(float delta_time) override - { - Vector2 move = {0.0f, 0.0f}; - - move = {GetGamepadAxisMovement(player_num, GAMEPAD_AXIS_LEFT_X), - GetGamepadAxisMovement(player_num, GAMEPAD_AXIS_LEFT_Y)}; - - if (IsKeyDown(KEY_W) || IsGamepadButtonDown(player_num, GAMEPAD_BUTTON_LEFT_FACE_UP)) - { - move.y -= 1.0f; - } - if (IsKeyDown(KEY_S) || IsGamepadButtonDown(player_num, GAMEPAD_BUTTON_LEFT_FACE_DOWN)) - { - move.y += 1.0f; - } - if (IsKeyDown(KEY_A) || IsGamepadButtonDown(player_num, GAMEPAD_BUTTON_LEFT_FACE_LEFT)) - { - move.x -= 1.0f; - } - if (IsKeyDown(KEY_D) || IsGamepadButtonDown(player_num, GAMEPAD_BUTTON_LEFT_FACE_RIGHT)) - { - move.x += 1.0f; - } - - movement->set_input(move.x, move.y); - - // Update sprite position and rotation. - sprite->set_position(body->get_position_pixels()); - sprite->set_rotation(movement->facing_dir); - - // Shooting - if (IsKeyPressed(KEY_SPACE) || IsGamepadButtonPressed(player_num, GAMEPAD_BUTTON_RIGHT_FACE_RIGHT)) - { - // Find an inactive bullet from the pool. - for (auto& bullet : bullets) - { - if (!bullet->is_active) - { - // Play shoot sound. - shoot_sound->play(); - - // Activate and position the bullet. - Vector2 char_pos = body->get_position_pixels(); - Vector2 shoot_dir = {std::cos(movement->facing_dir * DEG2RAD), - std::sin(movement->facing_dir * DEG2RAD)}; - Vector2 bullet_start_pos = {char_pos.x + shoot_dir.x * 48.0f, char_pos.y + shoot_dir.y * 48.0f}; - bullet->body->set_position(bullet_start_pos); - - bullet->body->set_rotation(movement->facing_dir + 90.0f); - - // Set bullet velocity. - Vector2 velocity = {shoot_dir.x * bullet->speed, shoot_dir.y * bullet->speed}; - bullet->body->set_velocity(velocity); - bullet->is_active = true; - break; - } - } - } - - // Damage. - auto contacts = body->get_contacts(); - for (const auto& contact : contacts) - { - GameObject* other = static_cast(b2Body_GetUserData(contact)); - if (other) - { - if (other->has_tag("zombie")) - { - // When we are in contact with a zombie long enough, take 1 damage. - if (contact_timer > 0.0f) - { - contact_timer -= delta_time; - } - if (contact_timer <= 0.0f) - { - health -= 1; - contact_timer = contact_cooldown; - if (health <= 0) - { - // Deactivate character. - is_active = false; - // Move off-screen. - body->set_position(Vector2{-1000.0f, -1000.0f}); - body->set_velocity(Vector2{0.0f, 0.0f}); - } - } - } - } - } - } -}; - -/** - * A zombie that chases the closest player. - */ -class Zombie : public GameObject -{ -public: - BodyComponent* body; - PhysicsService* physics; - SpriteComponent* sprite; - TopDownMovementComponent* movement; - std::vector> players; - - Zombie(std::vector> players) : players(std::move(players)) {} - - void init() override - { - // Grab the physics service. - physics = scene->get_service(); - - body = add_component( - [=](BodyComponent& b) - { - b2BodyDef body_def = b2DefaultBodyDef(); - body_def.type = b2_dynamicBody; - body_def.fixedRotation = true; - body_def.position = physics->convert_to_meters(Vector2{-1000.0f, -1000.0f}); - body_def.userData = this; - b.id = b2CreateBody(physics->world, &body_def); - - b2SurfaceMaterial body_material = b2DefaultSurfaceMaterial(); - - b2ShapeDef circle_shape_def = b2DefaultShapeDef(); - circle_shape_def.density = 1.0f; - circle_shape_def.material = body_material; - - b2Circle circle_shape = {b2Vec2_zero, physics->convert_to_meters(16.0f)}; - b2CreateCircleShape(b.id, &circle_shape_def, &circle_shape); - // Disable by default. - b2Body_Disable(b.id); - }); - - // Setup movement. - TopDownMovementParams mp; - mp.accel = 5000.0f; - mp.friction = 5000.0f; - mp.max_speed = 100.0f; - movement = add_component(mp); - - // Setup sprite. - sprite = add_component("assets/zombie_shooter/zombie.png"); - } - - void update(float delta_time) override - { - // Find the closest player and move towards them. - Vector2 closest_player_pos = {0, 0}; - float closest_dist_sq = FLT_MAX; - for (auto& player : players) - { - Vector2 player_pos = player->body->get_position_pixels(); - Vector2 to_player = {player_pos.x - body->get_position_pixels().x, - player_pos.y - body->get_position_pixels().y}; - float dist_sq = to_player.x * to_player.x + to_player.y * to_player.y; - if (dist_sq < closest_dist_sq) - { - closest_dist_sq = dist_sq; - closest_player_pos = player_pos; - } - } - Vector2 to_closest = {closest_player_pos.x - body->get_position_pixels().x, - closest_player_pos.y - body->get_position_pixels().y}; - float to_closest_len = std::sqrt(to_closest.x * to_closest.x + to_closest.y * to_closest.y); - if (to_closest_len > 0.0f) - { - to_closest.x /= to_closest_len; - to_closest.y /= to_closest_len; - } - movement->set_input(to_closest.x, to_closest.y); - - // Update sprite position and rotation. - sprite->set_position(body->get_position_pixels()); - sprite->set_rotation(movement->facing_dir); - } -}; - -/** - * A spawner that spawns zombies at intervals. - */ -class Spawner : public GameObject -{ -public: - float spawn_timer = 0.0f; - float spawn_interval = 1.0f; // Spawn a zombie every 1 second - std::vector> zombie_pool; - Vector2 position = {0, 0}; - Vector2 size = {0, 0}; - - Spawner(Vector2 position, Vector2 size, std::vector> zombies) : - position(position - size * 0.5f), - size(size), - zombie_pool(std::move(zombies)) - { - } - - void update(float delta_time) override - { - spawn_timer -= delta_time; - if (spawn_timer <= 0.0f) - { - spawn_timer = spawn_interval; - - // Spawn a zombie at a random position within the spawner area - float x = position.x + static_cast(GetRandomValue(0, static_cast(size.x))); - float y = position.y + static_cast(GetRandomValue(0, static_cast(size.y))); - Vector2 spawn_pos = {x, y}; - - for (auto& zombie : zombie_pool) - { - if (!zombie->is_active) - { - zombie->body->set_position(spawn_pos); - zombie->is_active = true; - zombie->body->enable(); - return; - } - } - } - } -}; - -/** - * A scene for the zombie shooter game. - */ -class ZombieScene : public Scene -{ -public: - FontManager* font_manager; - PhysicsService* physics; - LevelService* level; - RenderTexture2D renderer; - RenderTexture2D light_map; - Texture2D light_texture; - std::vector> bullets; - std::vector> characters; - std::vector> zombies; - - void init_services() override - { - // TextureService and SoundService are needed by other components and game objects. - add_service(); - add_service(); - - // Set gravity to zero for top-down game. - physics = add_service(b2Vec2_zero); - std::vector collision_names = {"walls", "obstacles"}; - level = add_service("assets/levels/top_down.ldtk", "Level", collision_names); - - // Grab the font manager. - font_manager = game->get_manager(); - } - - void init() override - { - const auto& entities_layer = level->get_layer_by_name("Entities"); - - // Prepare a pool of bullets. - // It is unwise to call init during update loops, so we create all bullets here and deactivate them. - for (int i = 0; i < 100; i++) - { - auto bullet = add_game_object(); - // When is_active is false, update and draw are skipped. - bullet->is_active = false; - bullets.push_back(bullet); - } - - // Create player characters. - auto player_entities = level->get_entities_by_name("Start"); - - for (int i = 0; i < player_entities.size() && i < 4; i++) - { - auto& player_entity = player_entities[i]; - auto position = level->convert_to_pixels(player_entity->getPosition()); - auto character = add_game_object(position, bullets, i); - character->add_tag("player"); - characters.push_back(character); - } - - // Prepare a pool of zombies. - // It is unwise to call init during update loops, so we create all zombies here and deactivate them. - for (int i = 0; i < 100; i++) - { - // Start off-screen - auto zombie = add_game_object(characters); - // When is_active is false, update and draw are skipped. - zombie->is_active = false; - zombie->add_tag("zombie"); - zombies.push_back(zombie); - } - - // Create spawner. - auto spawn_entity = level->get_entities_by_name("Spawn")[0]; - auto spawn_position = level->convert_to_pixels(spawn_entity->getPosition()); - auto spawn_size = level->convert_to_pixels(spawn_entity->getSize()); - auto spawner = add_game_object(spawn_position, spawn_size, zombies); - - // We want to control when the foreground layer is drawn. - level->set_layer_visibility("Foreground", false); - - // Create render texture to scale the level to the screen. - renderer = LoadRenderTexture((int)level->get_size().x, (int)level->get_size().y); - light_map = LoadRenderTexture((int)level->get_size().x, (int)level->get_size().y); - - light_texture = get_service()->get_texture("assets/zombie_shooter/light.png"); - } - - void update(float delta_time) override - { - // Trigger scene change on Enter key or gamepad start button. - if (IsKeyPressed(KEY_ENTER) || IsGamepadButtonPressed(0, GAMEPAD_BUTTON_MIDDLE_RIGHT)) - { - game->go_to_scene_next(); - } - } - - void draw_scene() override - { - // Build up the light mask - BeginTextureMode(light_map); - ClearBackground(BLACK); - - // Force the blend mode to only set the alpha of the destination - rlSetBlendFactors(RLGL_SRC_ALPHA, RLGL_SRC_ALPHA, RLGL_MIN); - rlSetBlendMode(BLEND_CUSTOM); - - // Merge in all the light masks - for (int i = 0; i < 4; i++) - { - // DrawCircleGradient((int)characters[i]->body->get_position_pixels().x, - // (int)characters[i]->body->get_position_pixels().y, - // 300.0f, - // ColorAlpha(WHITE, 0), - // ColorAlpha(BLACK, 1)); - DrawTexture(light_texture, - (int)(characters[i]->body->get_position_pixels().x - light_texture.width / 2), - (int)(characters[i]->body->get_position_pixels().y - light_texture.height / 2), - WHITE); - } - - rlDrawRenderBatchActive(); - - // Go back to normal blend - rlSetBlendMode(BLEND_ALPHA); - EndTextureMode(); - - // Draw to render texture first. - BeginTextureMode(renderer); - ClearBackground(MAGENTA); - Scene::draw_scene(); - level->draw_layer("Foreground"); - DrawTexturePro( - light_map.texture, - {0.0f, 0.0f, static_cast(light_map.texture.width), static_cast(-light_map.texture.height)}, - {0.0f, 0.0f, static_cast(light_map.texture.width), static_cast(light_map.texture.height)}, - {0.0f, 0.0f}, - 0.0f, - ColorAlpha(WHITE, 0.92f)); - DrawRectangle(10, 10, 210, 210, Fade(WHITE, 0.3f)); - DrawTextEx(font_manager->get_font("Roboto"), - TextFormat("Health: %d\nHealth: %d\nHealth: %d\nHealth: %d", - characters[0]->health, - characters[1]->health, - characters[2]->health, - characters[3]->health), - Vector2{20.0f, 20.0f}, - 45.0f, - 1.0f, - RED); - EndTextureMode(); - - // Draw the render texture scaled to the screen. - DrawTexturePro( - renderer.texture, - {0.0f, 0.0f, static_cast(renderer.texture.width), static_cast(-renderer.texture.height)}, - {0.0f, 0.0f, static_cast(GetScreenWidth()), static_cast(GetScreenHeight())}, - {0.0f, 0.0f}, - 0.0f, - WHITE); - } -};