Skip to content

Commit 271a1d8

Browse files
committed
feat: implement UIRenderer for drawing UI elements and integrate with enemy health bar system
1 parent fa5a02d commit 271a1d8

5 files changed

Lines changed: 170 additions & 124 deletions

File tree

src/common/systems/ui-renderer.hpp

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
#pragma once
2+
3+
#include <glad/gl.h>
4+
5+
#include <components/camera.hpp>
6+
#include <ecs/world.hpp>
7+
#include <glm/gtc/matrix_transform.hpp>
8+
#include <glm/vec2.hpp>
9+
#include <glm/vec4.hpp>
10+
#include <vector>
11+
12+
#include "../asset-loader.hpp"
13+
#include "../material/material.hpp"
14+
#include "../mesh/mesh.hpp"
15+
#include "../mesh/vertex.hpp"
16+
#include "../shader/shader.hpp"
17+
18+
namespace our {
19+
20+
struct ScreenPoint {
21+
glm::vec2 position;
22+
float depth;
23+
bool visible;
24+
};
25+
26+
class UIRenderer {
27+
Mesh* quad = nullptr;
28+
TintedMaterial* material = nullptr;
29+
30+
static Vertex makeVertex(const glm::vec3& position, const glm::vec2& texCoord) {
31+
Vertex vertex{};
32+
vertex.position = position;
33+
vertex.color = {255, 255, 255, 255};
34+
vertex.tex_coord = texCoord;
35+
vertex.normal = {0.0f, 0.0f, 1.0f};
36+
return vertex;
37+
}
38+
39+
public:
40+
void initialize() {
41+
material = new TintedMaterial();
42+
material->shader = AssetLoader<ShaderProgram>::get("tinted");
43+
material->pipelineState.blending.enabled = true;
44+
material->pipelineState.blending.sourceFactor = GL_SRC_ALPHA;
45+
material->pipelineState.blending.destinationFactor = GL_ONE_MINUS_SRC_ALPHA;
46+
material->pipelineState.depthTesting.enabled = false;
47+
material->pipelineState.faceCulling.enabled = false;
48+
material->pipelineState.depthMask = false;
49+
50+
std::vector<Vertex> vertices = {
51+
makeVertex({0.0f, 0.0f, 0.0f}, {0.0f, 1.0f}),
52+
makeVertex({1.0f, 0.0f, 0.0f}, {1.0f, 1.0f}),
53+
makeVertex({1.0f, 1.0f, 0.0f}, {1.0f, 0.0f}),
54+
makeVertex({0.0f, 1.0f, 0.0f}, {0.0f, 0.0f}),
55+
};
56+
std::vector<unsigned int> elements = {0, 1, 2, 2, 3, 0};
57+
quad = new Mesh(vertices, elements);
58+
}
59+
60+
void destroy() {
61+
delete quad;
62+
delete material;
63+
quad = nullptr;
64+
material = nullptr;
65+
}
66+
67+
glm::mat4 overlayProjection(glm::ivec2 framebufferSize) const {
68+
return glm::ortho(0.0f, static_cast<float>(framebufferSize.x), static_cast<float>(framebufferSize.y), 0.0f,
69+
1.0f, -1.0f);
70+
}
71+
72+
void drawRect(const glm::mat4& projection, const glm::vec2& position, const glm::vec2& size,
73+
const glm::vec4& color) const {
74+
if (!(quad && material && material->shader)) return;
75+
if (size.x <= 0.0f || size.y <= 0.0f || color.a <= 0.0f) return;
76+
77+
material->tint = color;
78+
material->setup();
79+
material->shader->set("transform", projection * glm::translate(glm::mat4(1.0f), glm::vec3(position, 0.0f)) *
80+
glm::scale(glm::mat4(1.0f), glm::vec3(size, 1.0f)));
81+
quad->draw();
82+
}
83+
84+
static CameraComponent* findActiveCamera(World* world) {
85+
if (!world) return nullptr;
86+
87+
for (Entity* entity : world->getEntities())
88+
if (auto* camera = entity->getComponent<CameraComponent>()) return camera;
89+
90+
return nullptr;
91+
}
92+
93+
static ScreenPoint worldToScreen(const glm::vec3& worldPos, const glm::mat4& viewProj,
94+
glm::ivec2 framebufferSize, float frustumMargin = 1.1f) {
95+
ScreenPoint result{{0.0f, 0.0f}, 0.0f, false};
96+
97+
glm::vec4 clip = viewProj * glm::vec4(worldPos, 1.0f);
98+
if (clip.w <= 0.0f) return result; // behind camera
99+
100+
glm::vec3 ndc = glm::vec3(clip) / clip.w;
101+
if (ndc.z < -1.0f || ndc.z > 1.0f) return result;
102+
if (ndc.x < -frustumMargin || ndc.x > frustumMargin) return result;
103+
if (ndc.y < -frustumMargin || ndc.y > frustumMargin) return result;
104+
105+
result.position = {
106+
(ndc.x * 0.5f + 0.5f) * framebufferSize.x,
107+
(1.0f - (ndc.y * 0.5f + 0.5f)) * framebufferSize.y,
108+
};
109+
110+
result.depth = ndc.z;
111+
result.visible = true;
112+
return result;
113+
}
114+
};
115+
116+
} // namespace our

src/game/components/health.cpp

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,21 @@
11
#include "health.hpp"
22

3+
#include <algorithm>
4+
35
namespace gameplay {
46

57
void HealthComponent::deserialize(const nlohmann::json& data) {
68
if (!data.is_object()) return;
79
maxHealth = data.value("maxHealth", maxHealth);
810
currentHealth = data.contains("currentHealth") ? data["currentHealth"].get<float>() : maxHealth;
911
invulnerabilityTimer = data.value("invulnerabilityTimer", invulnerabilityTimer);
12+
damageRevealTimer = data.value("damageRevealTimer", damageRevealTimer);
1013
isDead = data.value("isDead", isDead);
1114
}
1215

16+
float HealthComponent::getHealthRatio() const {
17+
if (maxHealth <= 0.0f) return 0.0f;
18+
return std::clamp(currentHealth / maxHealth, 0.0f, 1.0f);
19+
}
20+
1321
} // namespace gameplay

src/game/components/health.hpp

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,11 @@ namespace gameplay {
99
float maxHealth = 100.0f;
1010
float currentHealth = 100.0f;
1111
float invulnerabilityTimer = 0.0f;
12+
float damageRevealTimer = 0.0f;
1213
bool isDead = false;
1314

15+
float getHealthRatio() const;
16+
1417
static std::string getID() {
1518
return "Health";
1619
}

src/game/systems/enemy-health-bar.hpp

Lines changed: 27 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,12 @@
33
#include <algorithm>
44
#include <application.hpp>
55
#include <cmath>
6-
#include <components/camera.hpp>
76
#include <ecs/world.hpp>
87
#include <glm/common.hpp>
98
#include <glm/geometric.hpp>
10-
#include <glm/gtc/matrix_transform.hpp>
119
#include <glm/vec2.hpp>
1210
#include <json/json.hpp>
13-
#include <material/material.hpp>
14-
#include <mesh/mesh.hpp>
15-
#include <shader/shader.hpp>
16-
#include <vector>
11+
#include <systems/ui-renderer.hpp>
1712

1813
#include "../components/collider.hpp"
1914
#include "../components/enemy.hpp"
@@ -24,14 +19,9 @@ namespace gameplay {
2419
class EnemyHealthBarSystem {
2520
struct Config {
2621
bool enabled = true;
27-
bool showWhenDamagedOnly = true;
2822
float maxDistance = 24.0f;
2923
} config;
3024

31-
our::Mesh* rectangle = nullptr;
32-
our::ShaderProgram* shader = nullptr;
33-
our::TintedMaterial* material = nullptr;
34-
3525
static glm::vec4 healthColor(float ratio) {
3626
ratio = glm::clamp(ratio, 0.0f, 1.0f);
3727
glm::vec3 low = {0.84f, 0.18f, 0.24f};
@@ -53,33 +43,6 @@ namespace gameplay {
5343
return std::max(colliderHeight, scaleHeight);
5444
}
5545

56-
static our::CameraComponent* findCamera(our::World* world) {
57-
for (our::Entity* entity : world->getEntities())
58-
if (auto* camera = entity->getComponent<our::CameraComponent>(); camera) return camera;
59-
60-
return nullptr;
61-
}
62-
63-
static our::Vertex makeVertex(const glm::vec3& position, const glm::vec2& texCoord) {
64-
our::Vertex vertex{};
65-
vertex.position = position;
66-
vertex.color = {255, 255, 255, 255};
67-
vertex.tex_coord = texCoord;
68-
vertex.normal = {0.0f, 0.0f, 1.0f};
69-
return vertex;
70-
}
71-
72-
void drawRect(const glm::mat4& projection, const glm::vec2& position, const glm::vec2& size,
73-
const glm::vec4& tint) const {
74-
if (!(rectangle && material && shader) || size.x <= 0.0f || size.y <= 0.0f || tint.a <= 0.0f) return;
75-
76-
material->tint = tint;
77-
material->setup();
78-
shader->set("transform", projection * glm::translate(glm::mat4(1.0f), glm::vec3(position, 0.0f)) *
79-
glm::scale(glm::mat4(1.0f), glm::vec3(size, 1.0f)));
80-
rectangle->draw();
81-
}
82-
8346
public:
8447
void deserialize(const nlohmann::json& sceneConfig) {
8548
if (!(sceneConfig.contains("game") && sceneConfig["game"].contains("enemyHealthBars"))) return;
@@ -89,39 +52,14 @@ namespace gameplay {
8952

9053
config = {};
9154
config.enabled = healthBarConfig.value("enabled", config.enabled);
92-
config.showWhenDamagedOnly = healthBarConfig.value("showWhenDamagedOnly", config.showWhenDamagedOnly);
9355
config.maxDistance = healthBarConfig.value("maxDistance", config.maxDistance);
56+
config.maxDistance = std::max(0.0f, config.maxDistance);
9457
}
9558

96-
void initialize() {
97-
shader = new our::ShaderProgram();
98-
shader->attach("assets/shaders/tinted.vert", GL_VERTEX_SHADER);
99-
shader->attach("assets/shaders/tinted.frag", GL_FRAGMENT_SHADER);
100-
shader->link();
101-
102-
material = new our::TintedMaterial();
103-
material->shader = shader;
104-
material->pipelineState.blending.enabled = true;
105-
material->pipelineState.blending.sourceFactor = GL_SRC_ALPHA;
106-
material->pipelineState.blending.destinationFactor = GL_ONE_MINUS_SRC_ALPHA;
107-
material->pipelineState.depthTesting.enabled = false;
108-
material->pipelineState.faceCulling.enabled = false;
109-
material->pipelineState.depthMask = false;
110-
111-
std::vector<our::Vertex> vertices = {
112-
makeVertex({0.0f, 0.0f, 0.0f}, {0.0f, 1.0f}),
113-
makeVertex({1.0f, 0.0f, 0.0f}, {1.0f, 1.0f}),
114-
makeVertex({1.0f, 1.0f, 0.0f}, {1.0f, 0.0f}),
115-
makeVertex({0.0f, 1.0f, 0.0f}, {0.0f, 0.0f}),
116-
};
117-
std::vector<unsigned int> elements = {0, 1, 2, 2, 3, 0};
118-
rectangle = new our::Mesh(vertices, elements);
119-
}
120-
121-
void render(our::World* world, our::Application* app) const {
122-
if (!(config.enabled && world && app && rectangle && material && shader)) return;
59+
void render(our::World* world, our::Application* app, const our::UIRenderer& ui) const {
60+
if (!(config.enabled && world && app)) return;
12361

124-
our::CameraComponent* camera = findCamera(world);
62+
our::CameraComponent* camera = our::UIRenderer::findActiveCamera(world);
12563
if (!camera) return;
12664

12765
glm::ivec2 framebufferSize = app->getFrameBufferSize();
@@ -130,72 +68,61 @@ namespace gameplay {
13068
glm::mat4 view = camera->getViewMatrix();
13169
glm::mat4 proj = camera->getProjectionMatrix(framebufferSize);
13270
glm::mat4 viewProj = proj * view;
133-
glm::mat4 overlayProj = glm::ortho(0.0f, static_cast<float>(framebufferSize.x),
134-
static_cast<float>(framebufferSize.y), 0.0f, 1.0f, -1.0f);
71+
glm::mat4 overlayProj = ui.overlayProjection(framebufferSize);
13572
glm::vec3 cameraPos = glm::vec3(camera->getOwner()->getLocalToWorldMatrix() * glm::vec4(0, 0, 0, 1));
13673

13774
for (our::Entity* entity : world->getEntities()) {
13875
auto* health = entity->getComponent<HealthComponent>();
13976
if (!(entity->getComponent<EnemyComponent>() && health) || health->isDead || health->maxHealth <= 0.0f)
14077
continue;
14178

142-
float healthRatio = glm::clamp(health->currentHealth / health->maxHealth, 0.0f, 1.0f);
143-
if (config.showWhenDamagedOnly && healthRatio >= 0.999f) continue;
144-
79+
float healthRatio = health->getHealthRatio();
14580
glm::mat4 localToWorld = entity->getLocalToWorldMatrix();
14681
glm::vec3 enemyPosition = glm::vec3(localToWorld * glm::vec4(0, 0, 0, 1));
14782
float distance = glm::distance(enemyPosition, cameraPos);
148-
if (distance > config.maxDistance) continue;
14983

150-
ColliderComponent* collider = entity->getComponent<ColliderComponent>();
151-
float barHeightOffset = estimateBarHeight(entity, collider) + 0.35;
84+
bool visibleWhenDamaged = health->damageRevealTimer > 0.0f;
85+
bool visibleWhenClose = distance <= 8.0f;
15286

153-
glm::vec4 clipSpacePosition = viewProj * localToWorld * glm::vec4(0.0f, barHeightOffset, 0.0f, 1.0f);
154-
if (clipSpacePosition.w <= 0.0f) continue;
87+
if (!visibleWhenDamaged && distance > config.maxDistance) continue;
88+
if (!(visibleWhenDamaged || visibleWhenClose)) continue;
15589

156-
glm::vec3 ndc = glm::vec3(clipSpacePosition) / clipSpacePosition.w;
157-
if (ndc.z < -1.0f || ndc.z > 1.0f || ndc.x < -1.1f || ndc.x > 1.1f || ndc.y < -1.1f || ndc.y > 1.1f)
158-
continue;
90+
ColliderComponent* collider = entity->getComponent<ColliderComponent>();
91+
float barHeightOffset = estimateBarHeight(entity, collider) + 0.35f;
92+
93+
glm::vec3 barWorldPos = glm::vec3(localToWorld * glm::vec4(0.0f, barHeightOffset, 0.0f, 1.0f));
94+
our::ScreenPoint screen = our::UIRenderer::worldToScreen(barWorldPos, viewProj, framebufferSize);
95+
if (!screen.visible) continue;
15996

16097
float dist = glm::clamp((distance - 6.0f) / glm::max(1.0f, config.maxDistance - 6.0f), 0.0f, 1.0f);
161-
float alpha = 1.0f - glm::smoothstep(config.maxDistance * 0.7f, config.maxDistance, distance);
98+
float alpha = visibleWhenDamaged
99+
? 1.0f
100+
: 1.0f - glm::smoothstep(config.maxDistance * 0.7f, config.maxDistance, distance);
162101
if (alpha <= 0.0f) continue;
163102

164103
float barWidth = glm::mix(86.0f, 48.0f, dist);
165104
float barHeight = glm::mix(10.0f, 6.0f, dist);
166-
glm::vec2 screenCenter = {
167-
(ndc.x * 0.5f + 0.5f) * framebufferSize.x,
168-
(1.0f - (ndc.y * 0.5f + 0.5f)) * framebufferSize.y,
169-
};
170105

171-
glm::vec2 barPosition = {screenCenter.x - barWidth * 0.5f, screenCenter.y - barHeight};
106+
glm::vec2 barPosition = {screen.position.x - barWidth * 0.5f, screen.position.y - barHeight};
172107
glm::vec2 outerSize = {barWidth + 2.0f, barHeight + 2.0f};
173108
glm::vec2 innerPosition = barPosition + glm::vec2(2.0f, 2.0f);
174109
glm::vec2 innerSize = {barWidth - 4.0f, barHeight - 4.0f};
175110
glm::vec2 fillSize = {innerSize.x * healthRatio, innerSize.y};
176111

177-
drawRect(overlayProj, barPosition - glm::vec2(1.0f), outerSize, {0.02f, 0.03f, 0.05f, 0.92f * alpha});
178-
drawRect(overlayProj, barPosition, {barWidth, barHeight}, {0.12f, 0.14f, 0.18f, 0.88f * alpha});
112+
ui.drawRect(overlayProj, barPosition - glm::vec2(1.0f), outerSize,
113+
{0.02f, 0.03f, 0.05f, 0.92f * alpha});
114+
ui.drawRect(overlayProj, barPosition, {barWidth, barHeight}, {0.12f, 0.14f, 0.18f, 0.88f * alpha});
179115

180116
glm::vec4 fillColor = healthColor(healthRatio);
181117
fillColor.a = 0.96f * alpha;
182-
drawRect(overlayProj, innerPosition, fillSize, fillColor);
118+
ui.drawRect(overlayProj, innerPosition, fillSize, fillColor);
183119

184120
if (fillSize.x > 6.0f) {
185-
drawRect(overlayProj, innerPosition + glm::vec2(0.0f, 1.0f),
186-
{fillSize.x, std::max(1.0f, innerSize.y * 0.28f)}, {1.0f, 1.0f, 1.0f, 0.12f * alpha});
121+
ui.drawRect(overlayProj, innerPosition + glm::vec2(0.0f, 1.0f),
122+
{fillSize.x, std::max(1.0f, innerSize.y * 0.28f)}, {1.0f, 1.0f, 1.0f, 0.12f * alpha});
187123
}
188124
}
189125
}
190-
191-
void destroy() {
192-
delete rectangle;
193-
delete material;
194-
delete shader;
195-
rectangle = nullptr;
196-
material = nullptr;
197-
shader = nullptr;
198-
}
199126
};
200127

201128
} // namespace gameplay

0 commit comments

Comments
 (0)