From 361eecfcafe835e0378b9bd7ba0fef3e220720d2 Mon Sep 17 00:00:00 2001 From: Maxim Rodionov Date: Sun, 7 Dec 2025 02:16:09 +0300 Subject: [PATCH 1/2] feat: camera intersection detection to tile map --- engine/core/render.cpp | 66 ++++++++++++++++++++++++++++++++-------- engine/core/render.h | 16 +++++++++- game/loops/game_loop.cpp | 5 +-- game/loops/game_loop.h | 6 ++-- 4 files changed, 74 insertions(+), 19 deletions(-) diff --git a/engine/core/render.cpp b/engine/core/render.cpp index 497b8dc..264c66b 100644 --- a/engine/core/render.cpp +++ b/engine/core/render.cpp @@ -86,27 +86,33 @@ void Render::drawSprite(sf::RenderWindow &window, } void Render::generateTileMapVertices( - sf::VertexArray &vertices, Camera &camera, const std::vector &tiles, - int worldWidth, int worldHeight, + std::vector &tileMeshes, Camera &camera, + const std::vector &tiles, int worldWidth, int worldHeight, std::unordered_map &tileImages) { sf::Vector2f tileSize = camera.getTileSize(); float tileWidth = tileSize.x; float tileHeight = tileSize.y * 2.f; camera.setTileSize(tileWidth, tileHeight / 2); - vertices.setPrimitiveType(sf::PrimitiveType::Points); + const int step = 1; // If want to make draw faster, you can increase it + float zoom = camera.zoom; + int pointSize = static_cast(std::ceil(zoom)); - vertices.clear(); + tileMeshes.clear(); + tileMeshes.resize(worldWidth * worldHeight); auto getIndex = [&](int x, int y) { return y * worldWidth + x; }; - float zoom = camera.zoom; - int pointSize = static_cast(std::ceil(zoom)); - for (int y = 0; y < worldHeight; ++y) { for (int x = 0; x < worldWidth; ++x) { - const auto &tile = tiles[getIndex(x, y)]; + int index = getIndex(x, y); + + sf::VertexArray &mesh = tileMeshes[index]; + mesh.setPrimitiveType(sf::PrimitiveType::Points); + mesh.clear(); + + const auto &tile = tiles[index]; sf::Vector2f isoVec = camera.worldToScreen({(float)x, (float)y}); for (int layerId : tile.layerIds) { @@ -124,17 +130,16 @@ void Render::generateTileMapVertices( continue; sf::Color color = - tileImage.getPixel({(unsigned int)tx, (unsigned int)ty}); + tileImage.getPixel({(unsigned)tx, (unsigned)ty}); if (color.a == 0) continue; - float pixelX = isoVec.x + (static_cast(tx) * zoom); - float pixelY = isoVec.y + (static_cast(ty) * zoom) - - (static_cast(layerHeight) * zoom); + float pixelX = isoVec.x + tx * zoom; + float pixelY = isoVec.y + ty * zoom - layerHeight * zoom; for (int dy = 0; dy < pointSize; ++dy) { for (int dx = 0; dx < pointSize; ++dx) { - vertices.append({{pixelX + dx, pixelY + dy}, color}); + mesh.append({{pixelX + dx, pixelY + dy}, color}); } } } @@ -144,6 +149,41 @@ void Render::generateTileMapVertices( } } +void Render::renderMap(std::vector &tileMeshes, Camera &camera, + const sf::Vector2i wordSize, sf::VertexArray &tileVertices) { + tileVertices.clear(); + tileVertices.setPrimitiveType(sf::PrimitiveType::Points); + + sf::FloatRect cameraBounds = camera.getBounds(); + + sf::Vector2f rawTileSize = camera.getTileSize(); + + // Constants (10.f) is needed to account for tile overlapping + float scaledTileW = rawTileSize.x * camera.zoom + 10.f; + float scaledTileH = rawTileSize.y * 2.f * camera.zoom + 10.f; + + for (int y = 0; y < wordSize.y; ++y) { + for (int x = 0; x < wordSize.x; ++x) { + sf::Vector2f tilePos = camera.worldToScreen({(float)x, (float)y}); + + sf::FloatRect tileBounds({tilePos.x, tilePos.y - scaledTileH}, + {scaledTileW, scaledTileH * 2.f}); + + if (tileBounds.findIntersection(cameraBounds).has_value()) { + int index = y * wordSize.x + x; + + if (index < tileMeshes.size()) { + const sf::VertexArray &cachedMesh = tileMeshes[index]; + + for (std::size_t i = 0; i < cachedMesh.getVertexCount(); ++i) { + tileVertices.append(cachedMesh[i]); + } + } + } + } + } +} + void Render::drawFrame(const RenderFrame &frame) { window.clear(frame.clearColor); window.setView(frame.cameraView); diff --git a/engine/core/render.h b/engine/core/render.h index 87e8e8e..a3d599e 100644 --- a/engine/core/render.h +++ b/engine/core/render.h @@ -64,7 +64,7 @@ class Render { * @param tileImages Map of tile ID to tile visual data. */ void - generateTileMapVertices(sf::VertexArray &vertices, Camera &camera, + generateTileMapVertices(std::vector &tileMeshes, Camera &camera, const std::vector &tiles, int worldWidth, int worldHeight, std::unordered_map &tileImages); @@ -72,6 +72,20 @@ class Render { sf::RenderWindow &getWindow() { return window; } ///< Gets the render window void closeWindow() { window.close(); } ///< Closes the render window + /** + * @brief Renders the visible portion of the tilemap by collecting cached + * vertices. + * @param tileMeshes A vector containing cached VertexArrays (arrays of vertices) + * for each tile in the world. + * @param camera Reference to the camera for culling calculations. + * @param wordSize The dimensions of the tiled world (width and height in tiles). + * @param tileVertices A reference to the target sf::VertexArray to which the + * vertices of all visible tiles will be added. This array will be used for the + * final rendering of the frame. + */ + void renderMap(std::vector &tileMeshes, Camera &camera, + const sf::Vector2i wordSize, sf::VertexArray &tileVertices); + private: /** * @brief Draws an individual sprite to the window. diff --git a/game/loops/game_loop.cpp b/game/loops/game_loop.cpp index fe91bd3..6ba809d 100644 --- a/game/loops/game_loop.cpp +++ b/game/loops/game_loop.cpp @@ -54,7 +54,7 @@ void GameLoop::init() { } } - m_engine->render.generateTileMapVertices(m_staticMapPoints, m_engine->camera, + m_engine->render.generateTileMapVertices(m_tileMeshes, m_engine->camera, staticTiles, width, height, tileImages); // Create entities (player, NPC, etc.) @@ -128,7 +128,8 @@ void GameLoop::update(engine::Input &input, float dt) { void GameLoop::collectRenderData(engine::RenderFrame &frame, engine::Camera &camera) { // Collecting static map texture - frame.tileVertices = m_staticMapPoints; + m_engine->render.renderMap(m_tileMeshes, camera, sf::Vector2i({width, height}), + frame.tileVertices); // Collecting entities systems::renderSystem(m_registry, frame, camera, m_engine->imageManager); diff --git a/game/loops/game_loop.h b/game/loops/game_loop.h index 9a1fee4..410450a 100644 --- a/game/loops/game_loop.h +++ b/game/loops/game_loop.h @@ -91,9 +91,9 @@ class GameLoop : public engine::ILoop { int width; ///< World width in tile units int height; ///< World height in tile units std::unordered_map - tileTextures; ///< Tile ID to texture data mapping - sf::VertexArray m_staticMapPoints; ///< Pre-computed vertex data for static - ///< ground layer rendering + tileTextures; ///< Tile ID to texture data mapping + std::vector m_tileMeshes; ///< Pre-computed vertex data for + ///< static ground layer rendering std::vector tiles; ///< Tile data representing world layout, collision, and layers }; From 11d796520ad6ea7b34e84097264b31635b245ae0 Mon Sep 17 00:00:00 2001 From: Maxim Rodionov Date: Tue, 30 Dec 2025 01:27:33 +0300 Subject: [PATCH 2/2] docs: add perfomance report --- perfomance_report.md | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 perfomance_report.md diff --git a/perfomance_report.md b/perfomance_report.md new file mode 100644 index 0000000..05f14e6 --- /dev/null +++ b/perfomance_report.md @@ -0,0 +1,23 @@ +# Performance Report + +This demo shows the engine's performance on a [game written with it](https://github.com/Sibiri4ok/Half-Life-3), at a target resolution of `1600x900` pixels. + +**[Watch the performance demo](https://drive.google.com/file/d/1JZhuDTtMXrO5IbhyXu3KMq8SMozNSX9K/view?usp=sharing)** + +#### In the video: + +1) The required resolution (`1600x900`) being set in the game's code. +2) Gameplay with movement (running through the scene). +3) Real-time FPS (Frames Per Second) counter displayed in the terminal, demonstrating performance. +4) `htop` utility output, showing the overall CPU and memory load of the system during the benchmark. + +## Hardware + +- **Device:** Apple MacBook Air +- **Chip:** Apple M1 +- **RAM:** 8 GB + +## Core Optimization Implemented +The achieved performance is a direct result of `Frustum Culling` optimization: + +The engine performs visibility calculation for every frame. It renders only the entities and tiles that are currently within the camera's view (frustum). Objects outside the visible screen area are not processed by the render pipeline, significantly reducing the per-frame workload.