#include #include "JUI/Widgets/FpsGraph.hpp" #include #include "SimpleAABBSolver.hpp" TestGame::TestGameAppWindow::TestGameAppWindow(): ReWindow::OpenGLWindow( START_APP_TITLE, START_APP_WIDTH, START_APP_HEIGHT, GL_MAJOR, GL_MINOR), camera() { } TestGame::TestGameAppWindow::~TestGameAppWindow() { } template struct Range { T min; T max; }; template T map(T value, T in_min, T in_max, T out_min, T out_max) { return (value - in_min) / (in_max - in_min) * (out_max - out_min) + out_min; } template T map(T value, const Range& in, const Range& out) { return map(value, in.min, in.max, out.min, out.max); } void TestGame::TestGameAppWindow::CreateUI() { using namespace JUI::UDimLiterals; scene = new JUI::Scene(); auto* fps_graph = new JUI::FpsGraph(scene); // TODO: Revise to use PseudoDockedElementAtBottomOfViewport coming in the next JUI release. fps_graph->Size({100_percent, 50_px}); fps_graph->AnchorPoint({1, 1}); fps_graph->Position({100_percent, 100_percent}); fps_graph->BGColor(Colors::Transparent); fps_graph->BorderColor(Colors::Transparent); auto* phys_params_slider_window = new JUI::Window(scene); phys_params_slider_window->Size({300_px, 100_px}); phys_params_slider_window->Position({95_percent - 300_px, 95_percent-200_px}); phys_params_slider_window->Title("Physics Parameters"); auto* layout = new JUI::VerticalListLayout(phys_params_slider_window->Content()); auto* grav_slider = new LabeledSlider(layout); grav_slider->Size({100_percent, 20_px}); grav_slider->Minimum(0); grav_slider->Maximum(1); grav_slider->Interval(1.f/10000.f); grav_slider->CurrentValue(0.098f); grav_slider->TextColor(Colors::Black); grav_slider->Content("Gravity: 9.82"); grav_slider->ValueChanged += [&, grav_slider](float value) mutable { player->gravity = value * 100.f; grav_slider->Content(std::format("Gravity: {}", Math::Round(player->gravity, 3))); }; auto* air_resist_slider = new LabeledSlider(layout); air_resist_slider->Size({100_percent, 20_px}); air_resist_slider->Minimum(0); grav_slider->Maximum(1); air_resist_slider->Interval(1.f/10000.f); air_resist_slider->CurrentValue(0.12f); air_resist_slider->Content(std::format("Air Resistance: {}", 8)); air_resist_slider->TextColor(Colors::Black); air_resist_slider->ValueChanged += [&, air_resist_slider](float value) mutable { player->air_resistance = value * 100.f; air_resist_slider->Content(std::format("Air Resistance: {}", Math::Floor(player->air_resistance))); }; auto* mass_slider = new LabeledSlider(layout); mass_slider->Size({100_percent, 20_px}); mass_slider->Minimum(0); mass_slider->Maximum(1); mass_slider->Interval(1/10000.f); mass_slider->CurrentValue(0.08f); mass_slider->TextColor(Colors::Black); mass_slider->Content(std::format("Player Mass: {}", 12)); mass_slider->ValueChanged += [&, mass_slider](float value) mutable { player->mass = value * 100.f; mass_slider->Content(std::format("Player Mass: {}", player->mass)); }; auto* accel_slider = new LabeledSlider(layout); accel_slider->Size({100_percent, 20_px}); accel_slider->Minimum(0); accel_slider->Maximum(1); accel_slider->Interval(1/10000.f); accel_slider->CurrentValue(0.12f); accel_slider->TextColor(Colors::Black); accel_slider->Content(std::format("Player Accel: {}", 1200)); accel_slider->ValueChanged += [&, accel_slider](float value) mutable { player->acceleration = value * 10000.f; accel_slider->Content(std::format("Player Accel: {}", player->acceleration)); }; } void TestGame::TestGameAppWindow::Load() { Player::sprite = new JGL::Texture("assets/player.png", FilteringMode::NEAREST); CreateUI(); // TODO: More sophisticated order-of-operations is required. // 1. Initialize elements of widgets that are independent of the level itself / the level depends on. // 2. Initialize the level. // 3. Initialize widgets that are dependent on the level itself. loaded_level = new Level(std::filesystem::path("level.json")); loaded_tileset = new Tileset(std::filesystem::path(loaded_level->tileset_path)); loaded_tilesheet = new JGL::Texture(loaded_tileset->texture_path, FilteringMode::NEAREST); for (auto layer : loaded_level->layers) layer->Load(); data_ready = true; } bool TestGame::TestGameAppWindow::Open() { if (!OpenGLWindow::Open()) return false; auto size = GetSize(); auto vec_size = Vector2i(size.x, size.y); if (!JGL::Init(vec_size, 0.f, 1.f)) return false; JGL::Update(vec_size); glClearColor(0.f, 0.f, 0.f, 0.f); glEnable(GL_DEPTH_TEST); glDepthFunc(GL_LESS); glDepthMask(GL_TRUE); // TODO: Delete when we update to the next release of JGL glPixelStorei(GL_UNPACK_ALIGNMENT, 1); // NOTE: This MUST be called for text rendering to work properly!!! Load(); scene->SetViewportSize(Vector2(vec_size)); for (int i = 0; i < 100; i++) { auto* plr = new Player({50, 50}); entities.emplace_back(plr); player = plr; } camera.DefaultState(); return true; } void TestGame::TestGameAppWindow::CollisionSolve(float elapsed) { for (auto* entity : entities) { int coll_tests = 0; int coll_hits = 0; for (auto* layer : loaded_level->layers) { // TODO: if layer collides. int cell_width = layer->cell_width; int cell_height = layer->cell_height; int ent_tile_tl_x = Math::Floor(entity->next_pos.x) / cell_width; int ent_tile_tl_y = Math::Floor(entity->next_pos.y) / cell_height; int occupies_h_tiles = Math::Floor(entity->bbox.x) / cell_width; int occupies_v_tiles = Math::Floor(entity->bbox.y) / cell_height; Vector2 cell_bbox = Vector2(layer->cell_width, layer->cell_height); for (int x = -1; x <= occupies_h_tiles+1; x++) { for (int y = -1; y <= occupies_v_tiles+1; y++) { int cell_x = ent_tile_tl_x + x; int cell_y = ent_tile_tl_y + y; Vector2 cell_topleft = Vector2(cell_x*cell_width, cell_y*cell_height); if (cell_x < 0) continue; // Out of bounds to the left. if (cell_y < 0) continue; // Out of bounds to the top. if (cell_x >= layer->rows) continue; // Out of bounds to the right. if (cell_y >= layer->cols) continue;// Out of bounds to the bottom. auto cell = loaded_level->layers[0]->cells[cell_x][cell_y]; if (cell < 0) continue; // Empty cell. // Non-colliding tile. if (!loaded_tileset->tiles[cell].collides) continue; coll_tests++; auto cell_aabb = AABB2D(Vector2(cell_topleft), cell_bbox); Vector2 ent_halfbox = (entity->bbox / 2.f); Vector2 ent_centroid = entity->next_pos + ent_halfbox; Vector2 tile_halfbox = cell_bbox / 2.f; Vector2 tile_centroid = cell_topleft + tile_halfbox; if (Solver::AABB2Dvs(ent_centroid, ent_halfbox, tile_centroid, tile_halfbox)) { coll_hits++; Vector2 separation = Solver::SolveAABB(ent_centroid, ent_halfbox, tile_centroid, tile_halfbox); Vector2 normal = Solver::GetNormalForAABB(separation, entity->velocity); //if (normal.x == 0 && normal.y == 0) continue; // Why though? // Touched top. if (normal.y == -1) { entity->velocity.y = 0; } // Touched bottom. if (normal.y == 1) { entity->velocity.y *= -0.5f; } // Touched left, I think. if (normal.x == -1) { } // Touched right, I think. if (normal.x == -1) { } entity->next_pos += separation; } } } } } } void TestGame::TestGameAppWindow::CameraUpdate(float elapsed) { float move_rate = 120; float zoom_rate = 0.2f; if (IsKeyDown(Keys::Minus)) camera.scale.goal -= zoom_rate*elapsed; if (IsKeyDown(Keys::Equals)) camera.scale.goal += zoom_rate*elapsed; camera.Update(elapsed); } void TestGame::TestGameAppWindow::Update(float elapsed) { CameraUpdate(elapsed); camera.translation.goal = player->pos; scene->Update(elapsed); Vector2 window_dimensions(GetWidth(), GetHeight()); scene->SetViewportSize(window_dimensions); camera.screenSize = window_dimensions; JGL::Update(Vector2i(GetWidth(), GetHeight())); CollisionSolve(elapsed); for (auto* entity : entities) { entity->Update(elapsed); } if (!focused) std::this_thread::sleep_for(std::chrono::milliseconds(48)); } void TestGame::TestGameAppWindow::DrawLayer(const Layer *layer) const { for (int gx = 0; gx < layer->rows; gx++) { for (int gy = 0; gy < layer->cols; gy++) { auto quad_idx = layer->cells[gx][gy]; Vector2 pos(gx*layer->cell_width, gy*layer->cell_height); if (quad_idx < 0) continue; auto quad = loaded_tileset->quads[quad_idx]; J2D::DrawPartialSprite(loaded_tilesheet, pos, Vector2(quad.x, quad.y), Vector2(quad.w, quad.h)); } } } void TestGame::TestGameAppWindow::ApplyOriginTransformation() { glTranslatef(GetWidth() / 2.f, GetHeight() / 2.f, 0); } void TestGame::TestGameAppWindow::ApplyCameraTransformation() { ApplyOriginTransformation(); glRotatef(camera.rotation.current, 0, 0, 1); glScalef(camera.scale.current, camera.scale.current, 1); glTranslatef(-camera.translation.current.x, -camera.translation.current.y, 0); } void TestGame::TestGameAppWindow::DrawLevel(const Level *level) const { /// Draw top layers last. for (const auto* layer : std::ranges::views::reverse(level->layers)) { DrawLayer(layer); } } void TestGame::TestGameAppWindow::DrawWorldSpace() { if (!data_ready) return; // Don't try to draw the level if not loaded. DrawLevel(loaded_level); for (auto* entity : entities) { entity->Draw(); } } void TestGame::TestGameAppWindow::Render() { glClearColor(0, 0, 0, 1); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); J2D::Begin(); { glPushMatrix(); { ApplyCameraTransformation(); DrawWorldSpace(); } glPopMatrix(); } J2D::End(); scene->Draw(); }