This commit is contained in:
Vladislav Khorev 2025-12-30 15:52:26 +03:00
commit 0a5a3b653d
16 changed files with 27032 additions and 126 deletions

View File

@ -448,6 +448,10 @@ add_executable(space-game001
StoneObject.h
FrameBuffer.cpp
FrameBuffer.h
UiManager.cpp
UiManager.h
Projectile.h
Projectile.cpp
)
# Установка проекта по умолчанию для Visual Studio

322
Game.cpp
View File

@ -107,11 +107,10 @@ namespace ZL
, newTickCount(0)
, lastTickCount(0)
{
std::vector<Vector3f> emissionPoints = {
Vector3f{-2.1f, 0.9f, 5.0f},
Vector3f{2.1f, 0.9f, 5.0f}
};
sparkEmitter = SparkEmitter(emissionPoints, 100.0f);
projectiles.reserve(maxProjectiles);
for (int i = 0; i < maxProjectiles; ++i) {
projectiles.emplace_back(std::make_unique<Projectile>());
}
}
Game::~Game() {
@ -152,6 +151,45 @@ namespace ZL
#endif
bool cfgLoaded = sparkEmitter.loadFromJsonFile("../config/spark_config.json", renderer, CONST_ZIP_FILE);
bool projCfgLoaded = projectileEmitter.loadFromJsonFile("../config/spark_projectile_config.json", renderer, CONST_ZIP_FILE);
projectileEmitter.setEmissionPoints(std::vector<Vector3f>());
uiManager.loadFromFile("../config/ui.json", renderer, CONST_ZIP_FILE);
uiManager.setButtonCallback("playButton", [this](const std::string& name) {
std::cerr << "Play button pressed: " << name << std::endl;
});
uiManager.setButtonCallback("exitButton", [](const std::string& name) {
Environment::exitGameLoop = true;
});
uiManager.setButtonCallback("settingsButton", [this](const std::string& name) {
if (uiManager.pushMenuFromFile("../config/settings.json", this->renderer, CONST_ZIP_FILE)) {
uiManager.setButtonCallback("Opt1", [this](const std::string& n) {
std::cerr << "Opt1 pressed: " << n << std::endl;
});
uiManager.setButtonCallback("Opt2", [this](const std::string& n) {
std::cerr << "Opt2 pressed: " << n << std::endl;
});
uiManager.setButtonCallback("backButton", [this](const std::string& n) {
uiManager.popMenu();
});
}
else {
std::cerr << "Failed to open settings menu" << std::endl;
}
});
uiManager.setSliderCallback("musicVolumeSlider", [this](const std::string& name, float value) {
std::cerr << "Music volume slider changed to: " << value << std::endl;
musicVolume = value;
Environment::shipVelocity = musicVolume * 20.0f;
});
cubemapTexture = std::make_shared<Texture>(
std::array<TextureDataStruct, 6>{
CreateTextureDataFromBmp24("./resources/sky/space_rt.bmp", CONST_ZIP_FILE),
@ -192,67 +230,68 @@ namespace ZL
boxRenderArr[i].RefreshVBO();
}
sparkTexture = std::make_unique<Texture>(CreateTextureDataFromPng("./resources/spark.png", CONST_ZIP_FILE));
sparkEmitter.setTexture(sparkTexture);
if (!cfgLoaded)
{
throw std::runtime_error("Failed to load spark emitter config file!");
}
buttonTexture = std::make_unique<Texture>(CreateTextureDataFromPng("./resources/button.png", CONST_ZIP_FILE));
/* buttonTexture = std::make_unique<Texture>(CreateTextureDataFromPng("./resources/button.png", CONST_ZIP_FILE));
button.data.PositionData.push_back({ 100, 100, 0 });
button.data.PositionData.push_back({ 100, 150, 0 });
button.data.PositionData.push_back({ 300, 150, 0 });
button.data.PositionData.push_back({ 100, 100, 0 });
button.data.PositionData.push_back({ 300, 150, 0 });
button.data.PositionData.push_back({ 300, 100, 0 });
button.data.PositionData.push_back({ 100, 100, 0 });
button.data.PositionData.push_back({ 100, 150, 0 });
button.data.PositionData.push_back({ 300, 150, 0 });
button.data.PositionData.push_back({ 100, 100, 0 });
button.data.PositionData.push_back({ 300, 150, 0 });
button.data.PositionData.push_back({ 300, 100, 0 });
button.data.TexCoordData.push_back({ 0,0 });
button.data.TexCoordData.push_back({ 0,1 });
button.data.TexCoordData.push_back({ 1,1 });
button.data.TexCoordData.push_back({ 0,0 });
button.data.TexCoordData.push_back({ 1,1 });
button.data.TexCoordData.push_back({ 1,0 });
button.data.TexCoordData.push_back({ 0,0 });
button.data.TexCoordData.push_back({ 0,1 });
button.data.TexCoordData.push_back({ 1,1 });
button.data.TexCoordData.push_back({ 0,0 });
button.data.TexCoordData.push_back({ 1,1 });
button.data.TexCoordData.push_back({ 1,0 });
button.RefreshVBO();
button.RefreshVBO();*/
/*
musicVolumeBarTexture = std::make_unique<Texture>(CreateTextureDataFromPng("./resources/musicVolumeBarTexture.png", CONST_ZIP_FILE));
musicVolumeBarTexture = std::make_unique<Texture>(CreateTextureDataFromPng("./resources/musicVolumeBarTexture.png", CONST_ZIP_FILE));
musicVolumeBar.data.PositionData.push_back({ 1190, 100, 0 });
musicVolumeBar.data.PositionData.push_back({ 1190, 600, 0 });
musicVolumeBar.data.PositionData.push_back({ 1200, 600, 0 });
musicVolumeBar.data.PositionData.push_back({ 1190, 100, 0 });
musicVolumeBar.data.PositionData.push_back({ 1200, 600, 0 });
musicVolumeBar.data.PositionData.push_back({ 1200, 100, 0 });
musicVolumeBar.data.PositionData.push_back({ 1190, 100, 0 });
musicVolumeBar.data.PositionData.push_back({ 1190, 600, 0 });
musicVolumeBar.data.PositionData.push_back({ 1200, 600, 0 });
musicVolumeBar.data.PositionData.push_back({ 1190, 100, 0 });
musicVolumeBar.data.PositionData.push_back({ 1200, 600, 0 });
musicVolumeBar.data.PositionData.push_back({ 1200, 100, 0 });
musicVolumeBar.data.TexCoordData.push_back({ 0,0 });
musicVolumeBar.data.TexCoordData.push_back({ 0,1 });
musicVolumeBar.data.TexCoordData.push_back({ 1,1 });
musicVolumeBar.data.TexCoordData.push_back({ 0,0 });
musicVolumeBar.data.TexCoordData.push_back({ 1,1 });
musicVolumeBar.data.TexCoordData.push_back({ 1,0 });
musicVolumeBar.data.TexCoordData.push_back({ 0,0 });
musicVolumeBar.data.TexCoordData.push_back({ 0,1 });
musicVolumeBar.data.TexCoordData.push_back({ 1,1 });
musicVolumeBar.data.TexCoordData.push_back({ 0,0 });
musicVolumeBar.data.TexCoordData.push_back({ 1,1 });
musicVolumeBar.data.TexCoordData.push_back({ 1,0 });
musicVolumeBar.RefreshVBO();
musicVolumeBar.RefreshVBO();
musicVolumeBarButtonTexture = std::make_unique<Texture>(CreateTextureDataFromPng("./resources/musicVolumeBarButton.png", CONST_ZIP_FILE));
musicVolumeBarButtonTexture = std::make_unique<Texture>(CreateTextureDataFromPng("./resources/musicVolumeBarButton.png", CONST_ZIP_FILE));
float musicVolumeBarButtonButtonCenterY = 350.0f;
float musicVolumeBarButtonButtonCenterY = 350.0f;
musicVolumeBarButton.data.PositionData.push_back({ musicVolumeBarButtonButtonCenterX - musicVolumeBarButtonButtonRadius, musicVolumeBarButtonButtonCenterY - musicVolumeBarButtonButtonRadius, 0 });
musicVolumeBarButton.data.PositionData.push_back({ musicVolumeBarButtonButtonCenterX - musicVolumeBarButtonButtonRadius, musicVolumeBarButtonButtonCenterY + musicVolumeBarButtonButtonRadius, 0 });
musicVolumeBarButton.data.PositionData.push_back({ musicVolumeBarButtonButtonCenterX + musicVolumeBarButtonButtonRadius, musicVolumeBarButtonButtonCenterY + musicVolumeBarButtonButtonRadius, 0 });
musicVolumeBarButton.data.PositionData.push_back({ musicVolumeBarButtonButtonCenterX - musicVolumeBarButtonButtonRadius, musicVolumeBarButtonButtonCenterY - musicVolumeBarButtonButtonRadius, 0 });
musicVolumeBarButton.data.PositionData.push_back({ musicVolumeBarButtonButtonCenterX + musicVolumeBarButtonButtonRadius, musicVolumeBarButtonButtonCenterY + musicVolumeBarButtonButtonRadius, 0 });
musicVolumeBarButton.data.PositionData.push_back({ musicVolumeBarButtonButtonCenterX + musicVolumeBarButtonButtonRadius, musicVolumeBarButtonButtonCenterY - musicVolumeBarButtonButtonRadius, 0 });
musicVolumeBarButton.data.PositionData.push_back({ musicVolumeBarButtonButtonCenterX - musicVolumeBarButtonButtonRadius, musicVolumeBarButtonButtonCenterY - musicVolumeBarButtonButtonRadius, 0 });
musicVolumeBarButton.data.PositionData.push_back({ musicVolumeBarButtonButtonCenterX - musicVolumeBarButtonButtonRadius, musicVolumeBarButtonButtonCenterY + musicVolumeBarButtonButtonRadius, 0 });
musicVolumeBarButton.data.PositionData.push_back({ musicVolumeBarButtonButtonCenterX + musicVolumeBarButtonButtonRadius, musicVolumeBarButtonButtonCenterY + musicVolumeBarButtonButtonRadius, 0 });
musicVolumeBarButton.data.PositionData.push_back({ musicVolumeBarButtonButtonCenterX - musicVolumeBarButtonButtonRadius, musicVolumeBarButtonButtonCenterY - musicVolumeBarButtonButtonRadius, 0 });
musicVolumeBarButton.data.PositionData.push_back({ musicVolumeBarButtonButtonCenterX + musicVolumeBarButtonButtonRadius, musicVolumeBarButtonButtonCenterY + musicVolumeBarButtonButtonRadius, 0 });
musicVolumeBarButton.data.PositionData.push_back({ musicVolumeBarButtonButtonCenterX + musicVolumeBarButtonButtonRadius, musicVolumeBarButtonButtonCenterY - musicVolumeBarButtonButtonRadius, 0 });
musicVolumeBarButton.data.TexCoordData.push_back({ 0,0 });
musicVolumeBarButton.data.TexCoordData.push_back({ 0,1 });
musicVolumeBarButton.data.TexCoordData.push_back({ 1,1 });
musicVolumeBarButton.data.TexCoordData.push_back({ 0,0 });
musicVolumeBarButton.data.TexCoordData.push_back({ 1,1 });
musicVolumeBarButton.data.TexCoordData.push_back({ 1,0 });
musicVolumeBarButton.data.TexCoordData.push_back({ 0,0 });
musicVolumeBarButton.data.TexCoordData.push_back({ 0,1 });
musicVolumeBarButton.data.TexCoordData.push_back({ 1,1 });
musicVolumeBarButton.data.TexCoordData.push_back({ 0,0 });
musicVolumeBarButton.data.TexCoordData.push_back({ 1,1 });
musicVolumeBarButton.data.TexCoordData.push_back({ 1,0 });
musicVolumeBarButton.RefreshVBO();
musicVolumeBarButton.RefreshVBO();*/
renderer.InitOpenGL();
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
@ -320,10 +359,18 @@ namespace ZL
renderer.LoadIdentity();
renderer.TranslateMatrix({ 0,0, -1.0f * Environment::zoom });
renderer.TranslateMatrix({ 0, -Environment::zoom * 0.03f, 0 });
glBindTexture(GL_TEXTURE_2D, spaceshipTexture->getTexID());
renderer.DrawVertexRenderStruct(spaceship);
for (const auto& p : projectiles) {
if (p && p->isActive()) {
p->draw(renderer);
}
}
sparkEmitter.draw(renderer, Environment::zoom, Environment::width, Environment::height);
projectileEmitter.draw(renderer, Environment::zoom, Environment::width, Environment::height);
renderer.PopMatrix();
renderer.PopProjectionMatrix();
@ -376,7 +423,7 @@ namespace ZL
renderer.shaderManager.PopShader();
CheckGlError();
}
void Game::UpdateVolumeKnob() {
/*void Game::UpdateVolumeKnob() {
float musicVolumeBarButtonButtonCenterY = volumeBarMinY + musicVolume * (volumeBarMaxY - volumeBarMinY);
auto& pos = musicVolumeBarButton.data.PositionData;
@ -391,7 +438,6 @@ namespace ZL
musicVolumeBarButton.RefreshVBO();
}
void Game::UpdateVolumeFromMouse(int mouseX, int mouseY) {
int uiX = mouseX;
@ -407,7 +453,8 @@ namespace ZL
if (t > 1.0f) t = 1.0f;
musicVolume = t;
UpdateVolumeKnob();
}
}*/
void Game::drawUI()
{
static const std::string defaultShaderName = "default";
@ -416,7 +463,6 @@ namespace ZL
static const std::string vTexCoordName = "vTexCoord";
static const std::string textureUniformName = "Texture";
glClear(GL_DEPTH_BUFFER_BIT);
renderer.shaderManager.PushShader(defaultShaderName);
@ -424,26 +470,27 @@ namespace ZL
renderer.EnableVertexAttribArray(vPositionName);
renderer.EnableVertexAttribArray(vTexCoordName);
renderer.PushProjectionMatrix(Environment::width, Environment::height, -1, 1);
renderer.PushMatrix();
//renderer.PushProjectionMatrix(Environment::width, Environment::height, -1, 1);
//renderer.PushMatrix();
renderer.LoadIdentity();
//renderer.LoadIdentity();
//glBindTexture(GL_TEXTURE_2D, buttonTexture->getTexID());
//renderer.DrawVertexRenderStruct(button);
glBindTexture(GL_TEXTURE_2D, buttonTexture->getTexID());
renderer.DrawVertexRenderStruct(button);
//glBindTexture(GL_TEXTURE_2D, musicVolumeBarTexture->getTexID());
//renderer.DrawVertexRenderStruct(musicVolumeBar);
glBindTexture(GL_TEXTURE_2D, musicVolumeBarTexture->getTexID());
renderer.DrawVertexRenderStruct(musicVolumeBar);
//glBindTexture(GL_TEXTURE_2D, musicVolumeBarButtonTexture->getTexID());
//renderer.DrawVertexRenderStruct(musicVolumeBarButton);
glBindTexture(GL_TEXTURE_2D, musicVolumeBarButtonTexture->getTexID());
renderer.DrawVertexRenderStruct(musicVolumeBarButton);
//renderer.PopMatrix();
//renderer.PopProjectionMatrix();
renderer.PopMatrix();
renderer.PopProjectionMatrix();
renderer.DisableVertexAttribArray(vPositionName);
renderer.DisableVertexAttribArray(vTexCoordName);
uiManager.draw(renderer);
renderer.shaderManager.PopShader();
CheckGlError();
}
@ -513,7 +560,6 @@ namespace ZL
float diffx = Environment::tapDownCurrentPos.v[0] - Environment::tapDownStartPos.v[0];
float diffy = Environment::tapDownCurrentPos.v[1] - Environment::tapDownStartPos.v[1];
if (abs(diffy) > 5.0 || abs(diffx) > 5.0) //threshold
{
@ -545,14 +591,74 @@ namespace ZL
{
Vector3f velocityDirection = { 0,0, -Environment::shipVelocity * delta / 1000.f };
Vector3f velocityDirectionAdjusted = MultMatrixVector(Environment::shipMatrix, velocityDirection);
Environment::shipPosition = Environment::shipPosition + velocityDirectionAdjusted;
}
for (auto& p : projectiles) {
if (p && p->isActive()) {
p->update(static_cast<float>(delta), renderer);
}
}
std::vector<Vector3f> projCameraPoints;
for (const auto& p : projectiles) {
if (p && p->isActive()) {
Vector3f worldPos = p->getPosition();
Vector3f rel = worldPos - Environment::shipPosition;
Vector3f camPos = MultMatrixVector(Environment::inverseShipMatrix, rel);
projCameraPoints.push_back(camPos);
}
}
if (!projCameraPoints.empty()) {
projectileEmitter.setEmissionPoints(projCameraPoints);
projectileEmitter.emit();
}
else {
projectileEmitter.setEmissionPoints(std::vector<Vector3f>());
}
std::vector<Vector3f> shipCameraPoints;
for (const auto& lp : shipLocalEmissionPoints) {
Vector3f adjusted = lp + Vector3f{ 0.0f, -Environment::zoom * 0.03f, 0.0f };
shipCameraPoints.push_back(adjusted);
}
if (!shipCameraPoints.empty()) {
sparkEmitter.setEmissionPoints(shipCameraPoints);
}
sparkEmitter.update(static_cast<float>(delta));
projectileEmitter.update(static_cast<float>(delta));
lastTickCount = newTickCount;
}
}
void Game::fireProjectiles() {
std::vector<Vector3f> localOffsets = {
Vector3f{ -1.5f, 0.9f, 5.0f },
Vector3f{ 1.5f, 0.9f, 5.0f }
};
const float projectileSpeed = 60.0f;
const float lifeMs = 5000.0f;
const float size = 0.5f;
Vector3f localForward = { 0,0,-1 };
Vector3f worldForward = MultMatrixVector(Environment::shipMatrix, localForward).normalized();
for (const auto& lo : localOffsets) {
Vector3f worldPos = Environment::shipPosition + MultMatrixVector(Environment::shipMatrix, lo);
Vector3f worldVel = worldForward * projectileSpeed;
for (auto& p : projectiles) {
if (!p->isActive()) {
p->init(worldPos, worldVel, lifeMs, size, projectileTexture, renderer);
break;
}
}
}
}
void Game::render() {
SDL_GL_MakeCurrent(ZL::Environment::window, glContext);
ZL::CheckGlError();
@ -570,51 +676,73 @@ namespace ZL
while (SDL_PollEvent(&event)) {
if (event.type == SDL_QUIT) {
Environment::exitGameLoop = true;
}
else if (event.type == SDL_MOUSEBUTTONDOWN) {
// 1. Îáðàáîòêà íàæàòèÿ êíîïêè ìûøè
int mx = event.button.x;
int my = event.button.y;
std::cout << mx << " " << my << '\n';
int uiX = mx;
int uiY = Environment::height - my;
if (uiX >= volumeBarMinX - 40 && uiX <= volumeBarMaxX + 40 &&
uiY >= volumeBarMinY - 40 && uiY <= volumeBarMaxY + 40) {
isDraggingVolume = true;
UpdateVolumeFromMouse(mx, my);
}
else {
Environment::tapDownHold = true;
// Êîîðäèíàòû íà÷àëüíîãî íàæàòèÿ
Environment::tapDownStartPos.v[0] = event.button.x;
Environment::tapDownStartPos.v[1] = event.button.y;
// Íà÷àëüíàÿ ïîçèöèÿ òàêæå ñòàíîâèòñÿ òåêóùåé
Environment::tapDownCurrentPos.v[0] = event.button.x;
Environment::tapDownCurrentPos.v[1] = event.button.y;
uiManager.onMouseDown(uiX, uiY);
bool uiHandled = false;
if (event.button.button == SDL_BUTTON_LEFT && !uiManager.isUiInteraction()) {
uint64_t now = SDL_GetTicks64();
if (now - lastProjectileFireTime >= static_cast<uint64_t>(projectileCooldownMs)) {
lastProjectileFireTime = now;
fireProjectiles();
}
}
for (const auto& button : uiManager.findButton("") ? std::vector<std::shared_ptr<UiButton>>{} : std::vector<std::shared_ptr<UiButton>>{}) {
(void)button;
}
auto pressedSlider = [&]() -> std::shared_ptr<UiSlider> {
for (const auto& slider : uiManager.findSlider("") ? std::vector<std::shared_ptr<UiSlider>>{} : std::vector<std::shared_ptr<UiSlider>>{}) {
(void)slider;
}
return nullptr;
}();
if (!uiManager.isUiInteraction()) {
Environment::tapDownHold = true;
// Êîîðäèíàòû íà÷àëüíîãî íàæàòèÿ
Environment::tapDownStartPos.v[0] = mx;
Environment::tapDownStartPos.v[1] = my;
// Íà÷àëüíàÿ ïîçèöèÿ òàêæå ñòàíîâèòñÿ òåêóùåé
Environment::tapDownCurrentPos.v[0] = mx;
Environment::tapDownCurrentPos.v[1] = my;
}
}
else if (event.type == SDL_MOUSEBUTTONUP) {
// 2. Îáðàáîòêà îòïóñêàíèÿ êíîïêè ìûøè
isDraggingVolume = false;
Environment::tapDownHold = false;
int mx = event.button.x;
int my = event.button.y;
int uiX = mx;
int uiY = Environment::height - my;
uiManager.onMouseUp(uiX, uiY);
if (!uiManager.isUiInteraction()) {
Environment::tapDownHold = false;
}
}
else if (event.type == SDL_MOUSEMOTION) {
// 3. Îáðàáîòêà ïåðåìåùåíèÿ ìûøè
int mx = event.motion.x;
int my = event.motion.y;
int uiX = mx;
int uiY = Environment::height - my;
if (isDraggingVolume) {
// Äâèãàåì ìûøü ïî ñëàéäåðó — ìåíÿåì ãðîìêîñòü è ïîçèöèþ êðóæêà
UpdateVolumeFromMouse(mx, my);
}
if (Environment::tapDownHold) {
// Îáíîâëåíèå òåêóùåé ïîçèöèè, åñëè êíîïêà óäåðæèâàåòñÿ
Environment::tapDownCurrentPos.v[0] = event.motion.x;
Environment::tapDownCurrentPos.v[1] = event.motion.y;
uiManager.onMouseMove(uiX, uiY);
if (Environment::tapDownHold && !uiManager.isUiInteraction()) {
Environment::tapDownCurrentPos.v[0] = mx;
Environment::tapDownCurrentPos.v[1] = my;
}
}
else if (event.type == SDL_MOUSEWHEEL) {

37
Game.h
View File

@ -6,6 +6,8 @@
#include "TextureManager.h"
#include "SparkEmitter.h"
#include "PlanetObject.h"
#include "UiManager.h"
#include "Projectile.h"
namespace ZL {
@ -37,6 +39,8 @@ namespace ZL {
void drawBoxes();
void drawUI();
void fireProjectiles();
SDL_Window* window;
SDL_GLContext glContext;
Renderer renderer;
@ -52,26 +56,28 @@ namespace ZL {
std::vector<VertexRenderStruct> boxRenderArr;
std::shared_ptr<Texture> buttonTexture;
VertexRenderStruct button;
//std::shared_ptr<Texture> buttonTexture;
//VertexRenderStruct button;
std::shared_ptr<Texture> musicVolumeBarTexture;
VertexRenderStruct musicVolumeBar;
//std::shared_ptr<Texture> musicVolumeBarTexture;
//VertexRenderStruct musicVolumeBar;
std::shared_ptr<Texture> musicVolumeBarButtonTexture;
VertexRenderStruct musicVolumeBarButton;
//std::shared_ptr<Texture> musicVolumeBarButtonTexture;
//VertexRenderStruct musicVolumeBarButton;
bool isDraggingVolume = false;
//bool isDraggingVolume = false;
float musicVolume = 0.0f;
float volumeBarMinX = 1190.0f;
float volumeBarMaxX = 1200.0f;
float volumeBarMinY = 100.0f;
float volumeBarMaxY = 600.0f;
float musicVolumeBarButtonButtonCenterX = 1195.0f;
float musicVolumeBarButtonButtonRadius = 25.0f;
void UpdateVolumeFromMouse(int mouseX, int mouseY);
void UpdateVolumeKnob();
//float musicVolumeBarButtonButtonCenterX = 1195.0f;
//float musicVolumeBarButtonButtonRadius = 25.0f;
//void UpdateVolumeFromMouse(int mouseX, int mouseY);
//void UpdateVolumeKnob();
static const size_t CONST_TIMER_INTERVAL = 10;
static const size_t CONST_MAX_TIME_INTERVAL = 1000;
@ -88,7 +94,16 @@ namespace ZL {
VertexDataStruct boxBase;
SparkEmitter sparkEmitter;
SparkEmitter projectileEmitter;
PlanetObject planetObject;
UiManager uiManager;
std::vector<std::unique_ptr<Projectile>> projectiles;
std::shared_ptr<Texture> projectileTexture;
float projectileCooldownMs = 500.0f;
uint64_t lastProjectileFireTime = 0;
int maxProjectiles = 32;
std::vector<Vector3f> shipLocalEmissionPoints;
};

71
Projectile.cpp Normal file
View File

@ -0,0 +1,71 @@
#include "Projectile.h"
namespace ZL {
Projectile::Projectile()
: pos({ 0,0,0 })
, vel({ 0,0,0 })
, life(0.0f)
, maxLife(0.0f)
, active(false)
, size(0.5f)
, texture(nullptr)
{
}
void Projectile::init(const Vector3f& startPos, const Vector3f& startVel, float lifeMs, float s, std::shared_ptr<Texture> tex, Renderer& renderer) {
pos = startPos;
vel = startVel;
life = 0.0f;
maxLife = lifeMs;
size = s;
texture = tex;
active = true;
rebuildMesh(renderer);
mesh.RefreshVBO();
}
void Projectile::update(float deltaMs, Renderer& renderer) {
if (!active) return;
pos = pos + vel * (deltaMs / 1000.0f);
life += deltaMs;
if (life >= maxLife) {
active = false;
return;
}
rebuildMesh(renderer);
mesh.RefreshVBO();
}
void Projectile::rebuildMesh(Renderer&) {
float half = size * 0.5f;
mesh.data.PositionData.clear();
mesh.data.TexCoordData.clear();
mesh.data.PositionData.push_back({ pos.v[0] - half, pos.v[1] - half, pos.v[2] });
mesh.data.PositionData.push_back({ pos.v[0] - half, pos.v[1] + half, pos.v[2] });
mesh.data.PositionData.push_back({ pos.v[0] + half, pos.v[1] + half, pos.v[2] });
mesh.data.PositionData.push_back({ pos.v[0] - half, pos.v[1] - half, pos.v[2] });
mesh.data.PositionData.push_back({ pos.v[0] + half, pos.v[1] + half, pos.v[2] });
mesh.data.PositionData.push_back({ pos.v[0] + half, pos.v[1] - half, pos.v[2] });
mesh.data.TexCoordData.push_back({ 0.0f, 0.0f });
mesh.data.TexCoordData.push_back({ 0.0f, 1.0f });
mesh.data.TexCoordData.push_back({ 1.0f, 1.0f });
mesh.data.TexCoordData.push_back({ 0.0f, 0.0f });
mesh.data.TexCoordData.push_back({ 1.0f, 1.0f });
mesh.data.TexCoordData.push_back({ 1.0f, 0.0f });
}
void Projectile::draw(Renderer& renderer) const {
if (!active || !texture) return;
glBindTexture(GL_TEXTURE_2D, texture->getTexID());
renderer.DrawVertexRenderStruct(mesh);
}
} // namespace ZL

36
Projectile.h Normal file
View File

@ -0,0 +1,36 @@
#pragma once
#include "ZLMath.h"
#include "Renderer.h"
#include "TextureManager.h"
#include <memory>
namespace ZL {
class Projectile {
public:
Projectile();
~Projectile() = default;
void init(const Vector3f& startPos, const Vector3f& startVel, float lifeMs, float size, std::shared_ptr<Texture> tex, Renderer& renderer);
void update(float deltaMs, Renderer& renderer);
void draw(Renderer& renderer) const;
bool isActive() const { return active; }
Vector3f getPosition() const { return pos; }
private:
Vector3f pos;
Vector3f vel;
float life;
float maxLife;
bool active;
float size;
VertexRenderStruct mesh;
std::shared_ptr<Texture> texture;
void rebuildMesh(Renderer& renderer);
};
} // namespace ZL

View File

@ -2,13 +2,19 @@
#include <random>
#include <cmath>
#include "OpenGlExtensions.h"
#include <fstream>
#include "external/nlohmann/json.hpp"
#include <iostream>
#include "Environment.h"
#include <stdexcept>
namespace ZL {
using json = nlohmann::json;
SparkEmitter::SparkEmitter()
: emissionRate(100.0f), isActive(true), drawDataDirty(true), maxParticles(200) {
: emissionRate(100.0f), isActive(true), drawDataDirty(true), maxParticles(200),
shaderProgramName("default"), particleSize(0.04f), biasX(0.3f), configured(false) {
particles.resize(maxParticles);
drawPositions.reserve(maxParticles * 6);
drawTexCoords.reserve(maxParticles * 6);
@ -19,7 +25,8 @@ namespace ZL {
SparkEmitter::SparkEmitter(const std::vector<Vector3f>& positions, float rate)
: emissionPoints(positions), emissionRate(rate), isActive(true),
drawDataDirty(true), maxParticles(positions.size() * 100) {
drawDataDirty(true), maxParticles(positions.size() * 100),
shaderProgramName("default"), particleSize(0.04f), biasX(0.3f), configured(false) {
particles.resize(maxParticles);
drawPositions.reserve(maxParticles * 6);
drawTexCoords.reserve(maxParticles * 6);
@ -32,7 +39,8 @@ namespace ZL {
std::shared_ptr<Texture> tex,
float rate)
: emissionPoints(positions), texture(tex), emissionRate(rate),
isActive(true), drawDataDirty(true), maxParticles(positions.size() * 100) {
isActive(true), drawDataDirty(true), maxParticles(positions.size() * 100),
shaderProgramName("default"), particleSize(0.04f), biasX(0.3f), configured(false) {
particles.resize(maxParticles);
drawPositions.reserve(maxParticles * 6);
drawTexCoords.reserve(maxParticles * 6);
@ -73,7 +81,7 @@ namespace ZL {
for (const auto& [particlePtr, depth] : sortedParticles) {
const auto& particle = *particlePtr;
Vector3f pos = particle.position;
float size = 0.04f * particle.scale;
float size = particleSize * particle.scale;
drawPositions.push_back({ pos.v[0] - size, pos.v[1] - size, pos.v[2] });
drawTexCoords.push_back({ 0.0f, 0.0f });
@ -98,12 +106,16 @@ namespace ZL {
}
void SparkEmitter::draw(Renderer& renderer, float zoom, int screenWidth, int screenHeight) {
if (!configured) {
throw std::runtime_error("Failed to load spark emitter config file!");
}
if (getActiveParticleCount() == 0) {
return;
}
if (!texture) {
return;
throw std::runtime_error("Failed to load spark emitter config file!");
}
prepareDrawData();
@ -116,12 +128,11 @@ namespace ZL {
sparkQuad.data.TexCoordData = drawTexCoords;
sparkQuad.RefreshVBO();
static const std::string defaultShaderName = "default";
static const std::string vPositionName = "vPosition";
static const std::string vTexCoordName = "vTexCoord";
static const std::string textureUniformName = "Texture";
renderer.shaderManager.PushShader(defaultShaderName);
renderer.shaderManager.PushShader(shaderProgramName);
renderer.RenderUniform1i(textureUniformName, 0);
renderer.EnableVertexAttribArray(vPositionName);
renderer.EnableVertexAttribArray(vTexCoordName);
@ -132,7 +143,7 @@ namespace ZL {
glBindTexture(GL_TEXTURE_2D, texture->getTexID());
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE);// Аддитивное смешивание для эффекта свечения
glBlendFunc(GL_SRC_ALPHA, GL_ONE);
renderer.PushMatrix();
renderer.LoadIdentity();
@ -151,6 +162,10 @@ namespace ZL {
}
void SparkEmitter::update(float deltaTimeMs) {
if (!configured) {
throw std::runtime_error("Failed to load spark emitter config file!");
}
auto currentTime = std::chrono::steady_clock::now();
auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
currentTime - lastEmissionTime).count();
@ -197,6 +212,10 @@ namespace ZL {
}
void SparkEmitter::emit() {
if (!configured) {
throw std::runtime_error("Failed to load spark emitter config file!");
}
if (emissionPoints.empty()) return;
bool emitted = false;
@ -243,39 +262,51 @@ namespace ZL {
void SparkEmitter::setEmissionPoints(const std::vector<Vector3f>& positions) {
emissionPoints = positions;
maxParticles = positions.size() * 100;
particles.resize(maxParticles);
drawDataDirty = true;
}
void SparkEmitter::initParticle(SparkParticle& particle, int emitterIndex) {
if (!configured) {
throw std::runtime_error("Failed to load spark emitter config file!");
}
particle.velocity = getRandomVelocity(emitterIndex);
particle.scale = 1.0f;
particle.maxLifeTime = 800.0f + (rand() % 400);
static std::random_device rd;
static std::mt19937 gen(rd());
std::uniform_real_distribution<float> scaleDist(scaleRange.min, scaleRange.max);
particle.scale = scaleDist(gen);
std::uniform_real_distribution<float> lifeDist(lifeTimeRange.min, lifeTimeRange.max);
particle.maxLifeTime = lifeDist(gen);
particle.emitterIndex = emitterIndex;
}
Vector3f SparkEmitter::getRandomVelocity(int emitterIndex) {
if (!configured) {
throw std::runtime_error("Failed to load spark emitter config file!");
}
static std::random_device rd;
static std::mt19937 gen(rd());
static std::uniform_real_distribution<> angleDist(0, 2 * M_PI);
static std::uniform_real_distribution<> speedDist(0.5f, 2.0f);
static std::uniform_real_distribution<> zSpeedDist(1.0f, 3.0f);
std::uniform_real_distribution<float> angleDist(0.0f, 2.0f * static_cast<float>(M_PI));
std::uniform_real_distribution<float> speedDist(speedRange.min, speedRange.max);
std::uniform_real_distribution<float> zSpeedDist(zSpeedRange.min, zSpeedRange.max);
float angle = angleDist(gen);
float speed = speedDist(gen);
float zSpeed = zSpeedDist(gen);
// Теперь biasX берется из JSON
if (emitterIndex == 0) {
return Vector3f{
cosf(angle) * speed - 0.3f,
cosf(angle) * speed - biasX,
sinf(angle) * speed,
zSpeed
};
}
else {
return Vector3f{
cosf(angle) * speed + 0.3f,
cosf(angle) * speed + biasX,
sinf(angle) * speed,
zSpeed
};
@ -296,4 +327,197 @@ namespace ZL {
return count;
}
bool SparkEmitter::loadFromJsonFile(const std::string& path, Renderer& renderer, const std::string& zipFile) {
std::cout << "Loading spark config from: " << path << std::endl;
std::ifstream in(path);
if (!in.is_open()) {
std::cerr << "Failed to open JSON file: " << path << std::endl;
throw std::runtime_error("Failed to load spark emitter config file!");
}
json j;
try {
in >> j;
std::cout << "JSON parsed successfully" << std::endl;
}
catch (const std::exception& e) {
std::cerr << "JSON parse error: " << e.what() << std::endl;
throw std::runtime_error("Failed to load spark emitter config file!");
}
std::cout << "JSON content: " << j.dump(2) << std::endl;
auto requireKey = [&](const std::string& key) {
if (!j.contains(key)) {
std::cerr << "Missing required key in spark JSON: " << key << std::endl;
throw std::runtime_error("Failed to load spark emitter config file!");
}
};
requireKey("emissionPoints");
requireKey("texture");
requireKey("speedRange");
requireKey("zSpeedRange");
requireKey("scaleRange");
requireKey("lifeTimeRange");
requireKey("emissionRate");
requireKey("maxParticles");
requireKey("particleSize");
requireKey("biasX");
requireKey("shaderProgramName");
emissionRate = j["emissionRate"].get<float>();
maxParticles = j["maxParticles"].get<int>();
particles.resize(maxParticles);
drawPositions.reserve(maxParticles * 6);
drawTexCoords.reserve(maxParticles * 6);
std::cout << "Max particles: " << maxParticles << std::endl;
particleSize = j["particleSize"].get<float>();
std::cout << "Particle size: " << particleSize << std::endl;
biasX = j["biasX"].get<float>();
std::cout << "Bias X: " << biasX << std::endl;
// emissionPoints
std::vector<Vector3f> points;
if (j.contains("emissionPoints") && j["emissionPoints"].is_array()) {
for (const auto& el : j["emissionPoints"]) {
if (el.contains("position") && el["position"].is_array()) {
auto arr = el["position"];
points.push_back(Vector3f{ arr[0].get<float>(), arr[1].get<float>(), arr[2].get<float>() });
std::cout << "Fixed point: [" << arr[0] << ", " << arr[1] << ", " << arr[2] << "]" << std::endl;
}
else if (el.contains("positionRange") && el["positionRange"].is_object()) {
auto pr = el["positionRange"];
auto minArr = pr["min"];
auto maxArr = pr["max"];
int count = 1;
if (el.contains("count")) count = el["count"].get<int>();
std::random_device rd;
std::mt19937 gen(rd());
std::uniform_real_distribution<float> dx(minArr[0].get<float>(), maxArr[0].get<float>());
std::uniform_real_distribution<float> dy(minArr[1].get<float>(), maxArr[1].get<float>());
std::uniform_real_distribution<float> dz(minArr[2].get<float>(), maxArr[2].get<float>());
for (int k = 0; k < count; ++k) {
Vector3f randomPoint{ dx(gen), dy(gen), dz(gen) };
points.push_back(randomPoint);
std::cout << "Random point " << k + 1 << ": [" << randomPoint.v[0]
<< ", " << randomPoint.v[1] << ", " << randomPoint.v[2] << "]" << std::endl;
}
}
}
if (!points.empty()) {
setEmissionPoints(points);
std::cout << "Total emission points: " << emissionPoints.size() << std::endl;
}
else {
std::cerr << "Emission points parsed but empty" << std::endl;
throw std::runtime_error("Failed to load spark emitter config file!");
}
}
else {
std::cerr << "emissionPoints is not array" << std::endl;
throw std::runtime_error("Failed to load spark emitter config file!");
}
// Ranges
if (j.contains("speedRange") && j["speedRange"].is_array()) {
auto a = j["speedRange"];
speedRange.min = a[0].get<float>();
speedRange.max = a[1].get<float>();
std::cout << "Speed range: [" << speedRange.min << ", " << speedRange.max << "]" << std::endl;
}
else {
std::cerr << "speedRange missing or invalid" << std::endl;
throw std::runtime_error("Failed to load spark emitter config file!");
}
if (j.contains("zSpeedRange") && j["zSpeedRange"].is_array()) {
auto a = j["zSpeedRange"];
zSpeedRange.min = a[0].get<float>();
zSpeedRange.max = a[1].get<float>();
std::cout << "Z speed range: [" << zSpeedRange.min << ", " << zSpeedRange.max << "]" << std::endl;
}
else {
std::cerr << "zSpeedRange missing or invalid" << std::endl;
throw std::runtime_error("Failed to load spark emitter config file!");
}
if (j.contains("scaleRange") && j["scaleRange"].is_array()) {
auto a = j["scaleRange"];
scaleRange.min = a[0].get<float>();
scaleRange.max = a[1].get<float>();
std::cout << "Scale range: [" << scaleRange.min << ", " << scaleRange.max << "]" << std::endl;
}
else {
std::cerr << "scaleRange missing or invalid" << std::endl;
throw std::runtime_error("Failed to load spark emitter config file!");
}
if (j.contains("lifeTimeRange") && j["lifeTimeRange"].is_array()) {
auto a = j["lifeTimeRange"];
lifeTimeRange.min = a[0].get<float>();
lifeTimeRange.max = a[1].get<float>();
std::cout << "Life time range: [" << lifeTimeRange.min << ", " << lifeTimeRange.max << "]" << std::endl;
}
else {
std::cerr << "lifeTimeRange missing or invalid" << std::endl;
throw std::runtime_error("Failed to load spark emitter config file!");
}
// texture
if (j.contains("texture") && j["texture"].is_string()) {
std::string texPath = j["texture"].get<std::string>();
std::cout << "Loading texture: " << texPath << std::endl;
try {
auto texData = CreateTextureDataFromPng(texPath.c_str(), zipFile.c_str());
texture = std::make_shared<Texture>(texData);
std::cout << "Texture loaded successfully, ID: " << texture->getTexID() << std::endl;
}
catch (const std::exception& e) {
std::cerr << "Texture load error: " << e.what() << std::endl;
throw std::runtime_error("Failed to load spark emitter config file!");
}
}
else {
std::cerr << "No texture specified or invalid type in JSON" << std::endl;
throw std::runtime_error("Failed to load spark emitter config file!");
}
// shaders
if (j.contains("shaderProgramName") && j["shaderProgramName"].is_string()) {
shaderProgramName = j["shaderProgramName"].get<std::string>();
std::cout << "Shader program name: " << shaderProgramName << std::endl;
}
else {
std::cerr << "shaderProgramName missing or invalid" << std::endl;
throw std::runtime_error("Failed to load spark emitter config file!");
}
if (j.contains("vertexShader") && j.contains("fragmentShader")
&& j["vertexShader"].is_string() && j["fragmentShader"].is_string()) {
std::string v = j["vertexShader"].get<std::string>();
std::string f = j["fragmentShader"].get<std::string>();
std::cout << "Loading shaders - vertex: " << v << ", fragment: " << f << std::endl;
try {
renderer.shaderManager.AddShaderFromFiles(shaderProgramName, v, f, zipFile.c_str());
std::cout << "Shaders loaded successfully" << std::endl;
}
catch (const std::exception& e) {
std::cerr << "Shader load error: " << e.what() << std::endl;
throw std::runtime_error("Failed to load spark emitter config file!");
}
}
drawDataDirty = true;
configured = true;
std::cout << "SparkEmitter configuration loaded successfully!" << std::endl;
return true;
}
} // namespace ZL

View File

@ -5,6 +5,7 @@
#include "TextureManager.h"
#include <vector>
#include <chrono>
#include <string>
namespace ZL {
@ -37,7 +38,19 @@ namespace ZL {
std::shared_ptr<Texture> texture;
int maxParticles;
float particleSize;
float biasX;
// Ranges (used when config supplies intervals)
struct FloatRange { float min; float max; };
FloatRange speedRange; // XY speed
FloatRange zSpeedRange; // Z speed
FloatRange scaleRange;
FloatRange lifeTimeRange;
std::string shaderProgramName;
bool configured;
void prepareDrawData();
public:
@ -49,6 +62,13 @@ namespace ZL {
void setEmissionPoints(const std::vector<Vector3f>& positions);
void setTexture(std::shared_ptr<Texture> tex);
void setEmissionRate(float rate) { emissionRate = rate; }
void setShaderProgramName(const std::string& name) { shaderProgramName = name; }
void setMaxParticles(int max) { maxParticles = max; particles.resize(maxParticles); }
void setParticleSize(float size) { particleSize = size; }
void setBiasX(float bias) { biasX = bias; }
bool loadFromJsonFile(const std::string& path, Renderer& renderer, const std::string& zipFile = "");
void update(float deltaTimeMs);
void emit();
@ -57,6 +77,7 @@ namespace ZL {
const std::vector<SparkParticle>& getParticles() const;
size_t getActiveParticleCount() const;
std::shared_ptr<Texture> getTexture() const { return texture; }
private:
void initParticle(SparkParticle& particle, int emitterIndex);

530
UiManager.cpp Normal file
View File

@ -0,0 +1,530 @@
#include "UiManager.h"
#include "Utils.h"
#include <fstream>
#include <iostream>
#include <algorithm>
namespace ZL {
using json = nlohmann::json;
void UiButton::buildMesh() {
mesh.data.PositionData.clear();
mesh.data.TexCoordData.clear();
float x0 = rect.x;
float y0 = rect.y;
float x1 = rect.x + rect.w;
float y1 = rect.y + rect.h;
mesh.data.PositionData.push_back({ x0, y0, 0 });
mesh.data.TexCoordData.push_back({ 0, 0 });
mesh.data.PositionData.push_back({ x0, y1, 0 });
mesh.data.TexCoordData.push_back({ 0, 1 });
mesh.data.PositionData.push_back({ x1, y1, 0 });
mesh.data.TexCoordData.push_back({ 1, 1 });
mesh.data.PositionData.push_back({ x0, y0, 0 });
mesh.data.TexCoordData.push_back({ 0, 0 });
mesh.data.PositionData.push_back({ x1, y1, 0 });
mesh.data.TexCoordData.push_back({ 1, 1 });
mesh.data.PositionData.push_back({ x1, y0, 0 });
mesh.data.TexCoordData.push_back({ 1, 0 });
mesh.RefreshVBO();
}
void UiButton::draw(Renderer& renderer) const {
if (!texNormal) return;
const std::shared_ptr<Texture>* tex = &texNormal;
switch (state) {
case ButtonState::Normal: tex = &texNormal; break;
case ButtonState::Hover: tex = &texHover; break;
case ButtonState::Pressed: tex = &texPressed; break;
}
if (!(*tex)) return;
static const std::string vPositionName = "vPosition";
static const std::string vTexCoordName = "vTexCoord";
static const std::string textureUniformName = "Texture";
renderer.RenderUniform1i(textureUniformName, 0);
renderer.EnableVertexAttribArray(vPositionName);
renderer.EnableVertexAttribArray(vTexCoordName);
glBindTexture(GL_TEXTURE_2D, (*tex)->getTexID());
renderer.DrawVertexRenderStruct(mesh);
renderer.DisableVertexAttribArray(vPositionName);
renderer.DisableVertexAttribArray(vTexCoordName);
}
void UiSlider::buildTrackMesh() {
trackMesh.data.PositionData.clear();
trackMesh.data.TexCoordData.clear();
float x0 = rect.x;
float y0 = rect.y;
float x1 = rect.x + rect.w;
float y1 = rect.y + rect.h;
trackMesh.data.PositionData.push_back({ x0, y0, 0 });
trackMesh.data.TexCoordData.push_back({ 0, 0 });
trackMesh.data.PositionData.push_back({ x0, y1, 0 });
trackMesh.data.TexCoordData.push_back({ 0, 1 });
trackMesh.data.PositionData.push_back({ x1, y1, 0 });
trackMesh.data.TexCoordData.push_back({ 1, 1 });
trackMesh.data.PositionData.push_back({ x0, y0, 0 });
trackMesh.data.TexCoordData.push_back({ 0, 0 });
trackMesh.data.PositionData.push_back({ x1, y1, 0 });
trackMesh.data.TexCoordData.push_back({ 1, 1 });
trackMesh.data.PositionData.push_back({ x1, y0, 0 });
trackMesh.data.TexCoordData.push_back({ 1, 0 });
trackMesh.RefreshVBO();
}
void UiSlider::buildKnobMesh() {
knobMesh.data.PositionData.clear();
knobMesh.data.TexCoordData.clear();
float kw = vertical ? rect.w * 4.0f : rect.w * 0.5f;
float kh = vertical ? rect.w * 4.0f : rect.h * 0.5f;
float cx = rect.x + rect.w * 0.5f;
float cy = rect.y + (vertical ? (value * rect.h) : (rect.h * 0.5f));
float x0 = cx - kw * 0.5f;
float y0 = cy - kh * 0.5f;
float x1 = cx + kw * 0.5f;
float y1 = cy + kh * 0.5f;
knobMesh.data.PositionData.push_back({ x0, y0, 0 });
knobMesh.data.TexCoordData.push_back({ 0, 0 });
knobMesh.data.PositionData.push_back({ x0, y1, 0 });
knobMesh.data.TexCoordData.push_back({ 0, 1 });
knobMesh.data.PositionData.push_back({ x1, y1, 0 });
knobMesh.data.TexCoordData.push_back({ 1, 1 });
knobMesh.data.PositionData.push_back({ x0, y0, 0 });
knobMesh.data.TexCoordData.push_back({ 0, 0 });
knobMesh.data.PositionData.push_back({ x1, y1, 0 });
knobMesh.data.TexCoordData.push_back({ 1, 1 });
knobMesh.data.PositionData.push_back({ x1, y0, 0 });
knobMesh.data.TexCoordData.push_back({ 1, 0 });
knobMesh.RefreshVBO();
}
void UiSlider::draw(Renderer& renderer) const {
static const std::string vPositionName = "vPosition";
static const std::string vTexCoordName = "vTexCoord";
static const std::string textureUniformName = "Texture";
renderer.RenderUniform1i(textureUniformName, 0);
renderer.EnableVertexAttribArray(vPositionName);
renderer.EnableVertexAttribArray(vTexCoordName);
if (texTrack) {
glBindTexture(GL_TEXTURE_2D, texTrack->getTexID());
renderer.DrawVertexRenderStruct(trackMesh);
}
if (texKnob) {
glBindTexture(GL_TEXTURE_2D, texKnob->getTexID());
renderer.DrawVertexRenderStruct(knobMesh);
}
renderer.DisableVertexAttribArray(vPositionName);
renderer.DisableVertexAttribArray(vTexCoordName);
}
void UiManager::loadFromFile(const std::string& path, Renderer& renderer, const std::string& zipFile) {
std::ifstream in(path);
if (!in.is_open()) {
std::cerr << "UiManager: failed to open " << path << std::endl;
throw std::runtime_error("Failed to load UI file: " + path);
}
json j;
try {
in >> j;
}
catch (const std::exception& e) {
std::cerr << "UiManager: json parse error: " << e.what() << std::endl;
throw std::runtime_error("Failed to load UI file: " + path);
}
if (!j.contains("root") || !j["root"].is_object()) {
std::cerr << "UiManager: root node missing or invalid" << std::endl;
throw std::runtime_error("Failed to load UI file: " + path);
}
root = parseNode(j["root"], renderer, zipFile);
layoutNode(root);
buttons.clear();
sliders.clear();
collectButtonsAndSliders(root);
for (auto& b : buttons) {
b->buildMesh();
}
for (auto& s : sliders) {
s->buildTrackMesh();
s->buildKnobMesh();
}
}
std::shared_ptr<UiNode> UiManager::parseNode(const json& j, Renderer& renderer, const std::string& zipFile) {
auto node = std::make_shared<UiNode>();
if (j.contains("type") && j["type"].is_string()) node->type = j["type"].get<std::string>();
if (j.contains("name") && j["name"].is_string()) node->name = j["name"].get<std::string>();
if (j.contains("x")) node->rect.x = j["x"].get<float>();
if (j.contains("y")) node->rect.y = j["y"].get<float>();
if (j.contains("width")) node->rect.w = j["width"].get<float>();
if (j.contains("height")) node->rect.h = j["height"].get<float>();
if (j.contains("orientation") && j["orientation"].is_string()) node->orientation = j["orientation"].get<std::string>();
if (j.contains("spacing")) node->spacing = j["spacing"].get<float>();
if (node->type == "Button") {
auto btn = std::make_shared<UiButton>();
btn->name = node->name;
btn->rect = node->rect;
if (!j.contains("textures") || !j["textures"].is_object()) {
std::cerr << "UiManager: Button '" << btn->name << "' missing textures" << std::endl;
throw std::runtime_error("UI button textures missing");
}
auto t = j["textures"];
auto loadTex = [&](const std::string& key)->std::shared_ptr<Texture> {
if (!t.contains(key) || !t[key].is_string()) return nullptr;
std::string path = t[key].get<std::string>();
try {
auto data = CreateTextureDataFromPng(path.c_str(), zipFile.c_str());
return std::make_shared<Texture>(data);
}
catch (const std::exception& e) {
std::cerr << "UiManager: failed load texture " << path << " : " << e.what() << std::endl;
throw std::runtime_error("UI texture load failed: " + path);
}
};
btn->texNormal = loadTex("normal");
btn->texHover = loadTex("hover");
btn->texPressed = loadTex("pressed");
node->button = btn;
}
else if (node->type == "Slider") {
auto s = std::make_shared<UiSlider>();
s->name = node->name;
s->rect = node->rect;
if (!j.contains("textures") || !j["textures"].is_object()) {
std::cerr << "UiManager: Slider '" << s->name << "' missing textures" << std::endl;
throw std::runtime_error("UI slider textures missing");
}
auto t = j["textures"];
auto loadTex = [&](const std::string& key)->std::shared_ptr<Texture> {
if (!t.contains(key) || !t[key].is_string()) return nullptr;
std::string path = t[key].get<std::string>();
try {
auto data = CreateTextureDataFromPng(path.c_str(), zipFile.c_str());
return std::make_shared<Texture>(data);
}
catch (const std::exception& e) {
std::cerr << "UiManager: failed load texture " << path << " : " << e.what() << std::endl;
throw std::runtime_error("UI texture load failed: " + path);
}
};
s->texTrack = loadTex("track");
s->texKnob = loadTex("knob");
if (j.contains("value")) s->value = j["value"].get<float>();
if (j.contains("orientation")) {
std::string orient = j["orientation"].get<std::string>();
std::transform(orient.begin(), orient.end(), orient.begin(), ::tolower);
s->vertical = (orient != "horizontal");
}
node->slider = s;
}
if (j.contains("children") && j["children"].is_array()) {
for (const auto& ch : j["children"]) {
node->children.push_back(parseNode(ch, renderer, zipFile));
}
}
return node;
}
void UiManager::layoutNode(const std::shared_ptr<UiNode>& node) {
for (auto& child : node->children) {
child->rect.x += node->rect.x;
child->rect.y += node->rect.y;
}
if (node->type == "LinearLayout") {
std::string orient = node->orientation;
std::transform(orient.begin(), orient.end(), orient.begin(), ::tolower);
float cursorX = node->rect.x;
float cursorY = node->rect.y;
for (auto& child : node->children) {
if (orient == "horizontal") {
child->rect.x = cursorX;
child->rect.y = node->rect.y;
cursorX += child->rect.w + node->spacing;
}
else {
child->rect.x = node->rect.x;
child->rect.y = cursorY;
cursorY += child->rect.h + node->spacing;
}
layoutNode(child);
}
}
else {
for (auto& child : node->children) {
layoutNode(child);
}
}
}
void UiManager::collectButtonsAndSliders(const std::shared_ptr<UiNode>& node) {
if (node->button) {
buttons.push_back(node->button);
}
if (node->slider) {
sliders.push_back(node->slider);
}
for (auto& c : node->children) collectButtonsAndSliders(c);
}
bool UiManager::setButtonCallback(const std::string& name, std::function<void(const std::string&)> cb) {
auto b = findButton(name);
if (!b) {
std::cerr << "UiManager: setButtonCallback failed, button not found: " << name << std::endl;
return false;
}
b->onClick = std::move(cb);
return true;
}
bool UiManager::addSlider(const std::string& name, const UiRect& rect, Renderer& renderer, const std::string& zipFile,
const std::string& trackPath, const std::string& knobPath, float initialValue, bool vertical) {
auto s = std::make_shared<UiSlider>();
s->name = name;
s->rect = rect;
s->value = std::clamp(initialValue, 0.0f, 1.0f);
s->vertical = vertical;
try {
if (!trackPath.empty()) {
auto data = CreateTextureDataFromPng(trackPath.c_str(), zipFile.c_str());
s->texTrack = std::make_shared<Texture>(data);
}
if (!knobPath.empty()) {
auto data = CreateTextureDataFromPng(knobPath.c_str(), zipFile.c_str());
s->texKnob = std::make_shared<Texture>(data);
}
}
catch (const std::exception& e) {
std::cerr << "UiManager: addSlider failed to load textures: " << e.what() << std::endl;
return false;
}
s->buildTrackMesh();
s->buildKnobMesh();
sliders.push_back(s);
return true;
}
std::shared_ptr<UiSlider> UiManager::findSlider(const std::string& name) {
for (auto& s : sliders) if (s->name == name) return s;
return nullptr;
}
bool UiManager::setSliderCallback(const std::string& name, std::function<void(const std::string&, float)> cb) {
auto s = findSlider(name);
if (!s) {
std::cerr << "UiManager: setSliderCallback failed, slider not found: " << name << std::endl;
return false;
}
s->onValueChanged = std::move(cb);
return true;
}
bool UiManager::setSliderValue(const std::string& name, float value) {
auto s = findSlider(name);
if (!s) return false;
value = std::clamp(value, 0.0f, 1.0f);
if (fabs(s->value - value) < 1e-6f) return true;
s->value = value;
s->buildKnobMesh();
if (s->onValueChanged) s->onValueChanged(s->name, s->value);
return true;
}
bool UiManager::pushMenuFromFile(const std::string& path, Renderer& renderer, const std::string& zipFile) {
MenuState prev;
prev.root = root;
prev.buttons = buttons;
prev.sliders = sliders;
prev.pressedButton = pressedButton;
prev.pressedSlider = pressedSlider;
prev.path = "";
try {
loadFromFile(path, renderer, zipFile);
menuStack.push_back(std::move(prev));
return true;
}
catch (const std::exception& e) {
std::cerr << "UiManager: pushMenuFromFile failed to load " << path << " : " << e.what() << std::endl;
return false;
}
}
bool UiManager::popMenu() {
if (menuStack.empty()) {
std::cerr << "UiManager: popMenu called but menu stack is empty" << std::endl;
return false;
}
auto s = menuStack.back();
menuStack.pop_back();
root = s.root;
buttons = s.buttons;
sliders = s.sliders;
pressedButton = s.pressedButton;
pressedSlider = s.pressedSlider;
for (auto& b : buttons) {
if (b) b->buildMesh();
}
for (auto& sl : sliders) {
if (sl) { sl->buildTrackMesh(); sl->buildKnobMesh(); }
}
return true;
}
void UiManager::clearMenuStack() {
menuStack.clear();
}
void UiManager::draw(Renderer& renderer) {
renderer.PushProjectionMatrix(Environment::width, Environment::height, -1, 1);
renderer.PushMatrix();
renderer.LoadIdentity();
for (const auto& b : buttons) {
b->draw(renderer);
}
for (const auto& s : sliders) {
s->draw(renderer);
}
renderer.PopMatrix();
renderer.PopProjectionMatrix();
}
void UiManager::onMouseMove(int x, int y) {
for (auto& b : buttons) {
if (b->rect.contains((float)x, (float)y)) {
if (b->state != ButtonState::Pressed) b->state = ButtonState::Hover;
}
else {
if (b->state != ButtonState::Pressed) b->state = ButtonState::Normal;
}
}
if (pressedSlider) {
auto s = pressedSlider;
float t;
if (s->vertical) {
t = (y - s->rect.y) / s->rect.h;
}
else {
t = (x - s->rect.x) / s->rect.w;
}
if (t < 0.0f) t = 0.0f;
if (t > 1.0f) t = 1.0f;
s->value = t;
s->buildKnobMesh();
if (s->onValueChanged) s->onValueChanged(s->name, s->value);
}
}
void UiManager::onMouseDown(int x, int y) {
for (auto& b : buttons) {
if (b->rect.contains((float)x, (float)y)) {
b->state = ButtonState::Pressed;
pressedButton = b;
}
}
for (auto& s : sliders) {
if (s->rect.contains((float)x, (float)y)) {
pressedSlider = s;
float t;
if (s->vertical) {
t = (y - s->rect.y) / s->rect.h;
}
else {
t = (x - s->rect.x) / s->rect.w;
}
if (t < 0.0f) t = 0.0f;
if (t > 1.0f) t = 1.0f;
s->value = t;
s->buildKnobMesh();
if (s->onValueChanged) s->onValueChanged(s->name, s->value);
break;
}
}
}
void UiManager::onMouseUp(int x, int y) {
for (auto& b : buttons) {
bool contains = b->rect.contains((float)x, (float)y);
if (b->state == ButtonState::Pressed) {
if (contains && pressedButton == b) {
if (b->onClick) {
b->onClick(b->name);
}
}
b->state = contains ? ButtonState::Hover : ButtonState::Normal;
}
}
pressedButton.reset();
if (pressedSlider) {
pressedSlider.reset();
}
}
std::shared_ptr<UiButton> UiManager::findButton(const std::string& name) {
for (auto& b : buttons) if (b->name == name) return b;
return nullptr;
}
} // namespace ZL

133
UiManager.h Normal file
View File

@ -0,0 +1,133 @@
#pragma once
#include "Renderer.h"
#include "TextureManager.h"
#include "Environment.h"
#include "external/nlohmann/json.hpp"
#include <string>
#include <vector>
#include <memory>
#include <functional>
namespace ZL {
using json = nlohmann::json;
struct UiRect {
float x = 0;
float y = 0;
float w = 0;
float h = 0;
bool contains(float px, float py) const {
return px >= x && px <= x + w && py >= y && py <= y + h;
}
};
enum class ButtonState {
Normal,
Hover,
Pressed
};
struct UiButton {
std::string name;
UiRect rect;
std::shared_ptr<Texture> texNormal;
std::shared_ptr<Texture> texHover;
std::shared_ptr<Texture> texPressed;
ButtonState state = ButtonState::Normal;
VertexRenderStruct mesh;
std::function<void(const std::string&)> onClick;
void buildMesh();
void draw(Renderer& renderer) const;
};
struct UiSlider {
std::string name;
UiRect rect;
std::shared_ptr<Texture> texTrack;
std::shared_ptr<Texture> texKnob;
VertexRenderStruct trackMesh;
VertexRenderStruct knobMesh;
float value = 0.0f;
bool vertical = true;
std::function<void(const std::string&, float)> onValueChanged;
void buildTrackMesh();
void buildKnobMesh();
void draw(Renderer& renderer) const;
};
struct UiNode {
std::string type;
UiRect rect;
std::string name;
std::vector<std::shared_ptr<UiNode>> children;
std::shared_ptr<UiButton> button;
std::shared_ptr<UiSlider> slider;
std::string orientation = "vertical";
float spacing = 0.0f;
};
class UiManager {
public:
UiManager() = default;
void loadFromFile(const std::string& path, Renderer& renderer, const std::string& zipFile = "");
void draw(Renderer& renderer);
void onMouseMove(int x, int y);
void onMouseDown(int x, int y);
void onMouseUp(int x, int y);
bool isUiInteraction() const {
return pressedButton != nullptr || pressedSlider != nullptr;
}
std::shared_ptr<UiButton> findButton(const std::string& name);
bool setButtonCallback(const std::string& name, std::function<void(const std::string&)> cb);
bool addSlider(const std::string& name, const UiRect& rect, Renderer& renderer, const std::string& zipFile,
const std::string& trackPath, const std::string& knobPath, float initialValue = 0.0f, bool vertical = true);
std::shared_ptr<UiSlider> findSlider(const std::string& name);
bool setSliderCallback(const std::string& name, std::function<void(const std::string&, float)> cb);
bool setSliderValue(const std::string& name, float value); // programmatic set (clamped 0..1)
bool pushMenuFromFile(const std::string& path, Renderer& renderer, const std::string& zipFile = "");
bool popMenu();
void clearMenuStack();
private:
std::shared_ptr<UiNode> parseNode(const json& j, Renderer& renderer, const std::string& zipFile);
void layoutNode(const std::shared_ptr<UiNode>& node);
void collectButtonsAndSliders(const std::shared_ptr<UiNode>& node);
std::shared_ptr<UiNode> root;
std::vector<std::shared_ptr<UiButton>> buttons;
std::vector<std::shared_ptr<UiSlider>> sliders;
std::shared_ptr<UiButton> pressedButton;
std::shared_ptr<UiSlider> pressedSlider;
struct MenuState {
std::shared_ptr<UiNode> root;
std::vector<std::shared_ptr<UiButton>> buttons;
std::vector<std::shared_ptr<UiSlider>> sliders;
std::shared_ptr<UiButton> pressedButton;
std::shared_ptr<UiSlider> pressedSlider;
std::string path;
};
std::vector<MenuState> menuStack;
};
} // namespace ZL

72
config/settings.json Normal file
View File

@ -0,0 +1,72 @@
{
"root": {
"type": "FrameLayout",
"x": 0,
"y": 0,
"width": 1280,
"height": 720,
"children": [
{
"type": "FrameLayout",
"name": "centerPanel",
"x": 480,
"y": 160,
"width": 320,
"height": 400,
"children": [
{
"type": "LinearLayout",
"name": "settingsButtons",
"orientation": "vertical",
"spacing": 10,
"x": 0,
"y": 0,
"width": 300,
"height": 300,
"children": [
{
"type": "Button",
"name": "Opt1",
"x": 100,
"y": 300,
"width": 200,
"height": 50,
"textures": {
"normal": "./resources/button.png",
"hover": "./resources/button.png",
"pressed": "./resources/button.png"
}
},
{
"type": "Button",
"name": "Opt2",
"x": 100,
"y": 200,
"width": 200,
"height": 50,
"textures": {
"normal": "./resources/button.png",
"hover": "./resources/button.png",
"pressed": "./resources/button.png"
}
},
{
"type": "Button",
"name": "backButton",
"x": 100,
"y": 100,
"width": 200,
"height": 50,
"textures": {
"normal": "./resources/button.png",
"hover": "./resources/button.png",
"pressed": "./resources/button.png"
}
}
]
}
]
}
]
}
}

22
config/spark_config.json Normal file
View File

@ -0,0 +1,22 @@
{
"emissionRate": 100.0,
"maxParticles": 200,
"particleSize": 0.04,
"biasX": 0.3,
"emissionPoints": [
{
"position": [-2.1, 0.4, 5.0]
},
{
"position": [2.1, 0.4, 5.0]
}
],
"speedRange": [0.5, 2.0],
"zSpeedRange": [1.0, 3.0],
"scaleRange": [0.8, 1.2],
"lifeTimeRange": [600.0, 1400.0],
"texture": "./resources/spark.png",
"shaderProgramName": "default",
"vertexShader": "./shaders/spark.vertex",
"fragmentShader": "./shaders/spark.fragment"
}

View File

@ -0,0 +1,15 @@
{
"emissionPoints": [
{ "position": [0.0, 0.0, 0.0] }
],
"texture": "./resources/sand.png",
"speedRange": [10.0, 30.0],
"zSpeedRange": [-1.0, 1.0],
"scaleRange": [0.5, 1.0],
"lifeTimeRange": [200.0, 800.0],
"emissionRate": 50.0,
"maxParticles": 10,
"particleSize": 0.09,
"biasX": 0.1,
"shaderProgramName": "default"
}

86
config/ui.json Normal file
View File

@ -0,0 +1,86 @@
{
"root": {
"type": "FrameLayout",
"x": 0,
"y": 0,
"width": 1280,
"height": 720,
"children": [
{
"type": "FrameLayout",
"name": "leftPanel",
"x": 100,
"y": 100,
"width": 320,
"height": 400,
"children": [
{
"type": "LinearLayout",
"name": "mainButtons",
"orientation": "vertical",
"spacing": 10,
"x": 0,
"y": 0,
"width": 300,
"height": 300,
"children": [
{
"type": "Button",
"name": "playButton",
"x": 100,
"y": 300,
"width": 200,
"height": 50,
"textures": {
"normal": "./resources/button.png",
"hover": "./resources/button.png",
"pressed": "./resources/button.png"
}
},
{
"type": "Button",
"name": "settingsButton",
"x": 100,
"y": 200,
"width": 200,
"height": 50,
"textures": {
"normal": "./resources/sand.png",
"hover": "./resources/sand.png",
"pressed": "./resources/sand.png"
}
},
{
"type": "Button",
"name": "exitButton",
"x": 100,
"y": 100,
"width": 200,
"height": 50,
"textures": {
"normal": "./resources/rock.png",
"hover": "./resources/rock.png",
"pressed": "./resources/rock.png"
}
}
]
}
]
},
{
"type": "Slider",
"name": "musicVolumeSlider",
"x": 1140,
"y": 100,
"width": 10,
"height": 500,
"value": 0.5,
"orientation": "vertical",
"textures": {
"track": "./resources/musicVolumeBarTexture.png",
"knob": "./resources/musicVolumeBarButton.png"
}
}
]
}
}

25526
external/nlohmann/json.hpp vendored Normal file

File diff suppressed because it is too large Load Diff

12
shaders/spark.fragment Normal file
View File

@ -0,0 +1,12 @@
//precision mediump float;
uniform sampler2D Texture;
varying vec2 texCoord;
void main()
{
vec4 color = texture2D(Texture,texCoord).rgba;
//gl_FragColor = vec4(color.rgb*0.9 + vec3(0.1, 0.1, 0.1), 1.0);
gl_FragColor = color;
}

11
shaders/spark.vertex Normal file
View File

@ -0,0 +1,11 @@
attribute vec3 vPosition;
attribute vec2 vTexCoord;
varying vec2 texCoord;
uniform mat4 ProjectionModelViewMatrix;
void main()
{
gl_Position = ProjectionModelViewMatrix * vec4(vPosition.xyz, 1.0);
texCoord = vTexCoord;
}