Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 53 additions & 13 deletions engine/core/render.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -86,27 +86,33 @@ void Render::drawSprite(sf::RenderWindow &window,
}

void Render::generateTileMapVertices(
sf::VertexArray &vertices, Camera &camera, const std::vector<Tile> &tiles,
int worldWidth, int worldHeight,
std::vector<sf::VertexArray> &tileMeshes, Camera &camera,
const std::vector<Tile> &tiles, int worldWidth, int worldHeight,
std::unordered_map<int, engine::TileData> &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<int>(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<int>(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) {
Expand All @@ -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<float>(tx) * zoom);
float pixelY = isoVec.y + (static_cast<float>(ty) * zoom) -
(static_cast<float>(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});
}
}
}
Expand All @@ -144,6 +149,41 @@ void Render::generateTileMapVertices(
}
}

void Render::renderMap(std::vector<sf::VertexArray> &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);
Expand Down
16 changes: 15 additions & 1 deletion engine/core/render.h
Original file line number Diff line number Diff line change
Expand Up @@ -64,14 +64,28 @@ class Render {
* @param tileImages Map of tile ID to tile visual data.
*/
void
generateTileMapVertices(sf::VertexArray &vertices, Camera &camera,
generateTileMapVertices(std::vector<sf::VertexArray> &tileMeshes, Camera &camera,
const std::vector<Tile> &tiles, int worldWidth,
int worldHeight,
std::unordered_map<int, engine::TileData> &tileImages);

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<sf::VertexArray> &tileMeshes, Camera &camera,
const sf::Vector2i wordSize, sf::VertexArray &tileVertices);

private:
/**
* @brief Draws an individual sprite to the window.
Expand Down
5 changes: 3 additions & 2 deletions game/loops/game_loop.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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.)
Expand Down Expand Up @@ -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);
Expand Down
6 changes: 3 additions & 3 deletions game/loops/game_loop.h
Original file line number Diff line number Diff line change
Expand Up @@ -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<int, engine::TileTexture>
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<sf::VertexArray> m_tileMeshes; ///< Pre-computed vertex data for
///< static ground layer rendering
std::vector<engine::Tile>
tiles; ///< Tile data representing world layout, collision, and layers
};
23 changes: 23 additions & 0 deletions perfomance_report.md
Original file line number Diff line number Diff line change
@@ -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.