diff --git a/resources/config/ui.json b/resources/config/ui.json index 45428d0..bc3b266 100644 --- a/resources/config/ui.json +++ b/resources/config/ui.json @@ -154,8 +154,8 @@ { "type": "Button", "name": "shootButton", - "x": 100, - "y": 100, + "x": 1115, + "y": 0, "width": 100, "height": 100, "textures": { diff --git a/resources/joystick_base.png b/resources/joystick_base.png new file mode 100644 index 0000000..0f3633f --- /dev/null +++ b/resources/joystick_base.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bd8262a04d9bc1e5abbab731c7ea16b1fcedf513870fd9a7d0c5f6579f3acd67 +size 2342376 diff --git a/resources/joystick_knob.png b/resources/joystick_knob.png new file mode 100644 index 0000000..f296d52 --- /dev/null +++ b/resources/joystick_knob.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:30a9afd718e4f669510d4287d83c455e44cf62663adda818fc24cc6bffccc375 +size 499554 diff --git a/src/Game.cpp b/src/Game.cpp index 916b79a..729fde4 100644 --- a/src/Game.cpp +++ b/src/Game.cpp @@ -1,4 +1,4 @@ -#include "Game.h" +#include "Game.h" #include "AnimatedModel.h" #include "BoneAnimatedModel.h" #include "utils/Utils.h" @@ -281,6 +281,13 @@ namespace ZL Environment::shipVelocity = value * 1000.f; }); + // Добавляем джойстик для управления кораблём + // centerX=150, centerY=150 (от левого нижнего угла в UI координатах) + // baseRadius=120, knobRadius=50 + uiManager.addJoystick("shipJoystick", 150.0f, 150.0f, 120.0f, 50.0f, + renderer, CONST_ZIP_FILE, + "resources/joystick_base.png", "resources/joystick_knob.png"); + /*uiManager.setSliderCallback("musicVolumeSlider", [this](const std::string& name, float value) { std::cerr << "Music volume slider changed to: " << value << std::endl; musicVolume = value; @@ -604,36 +611,44 @@ namespace ZL sparkEmitter.update(static_cast(delta)); planetObject.update(static_cast(delta)); - if (Environment::tapDownHold && !uiManager.isUiInteraction()) + // Управление кораблём через джойстик + auto joystick = uiManager.findJoystick("shipJoystick"); + if (joystick && joystick->isActive) { + float joyX = joystick->getDirectionX(); // -1..1 + float joyY = joystick->getDirectionY(); // -1..1 + float magnitude = joystick->getMagnitude(); // 0..1 + + if (magnitude > 0.1f) { + // Скорость пропорциональна отклонению джойстика + Environment::shipVelocity = magnitude * 500.0f * static_cast(delta) / 100.0f; + + // Угол поворота корабля на основе направления джойстика + // joyY отрицательный = вперёд, joyX = влево/вправо + float angleY = std::atan2(-joyX, -joyY); + + Eigen::Quaternionf q(Eigen::AngleAxisf(angleY, Eigen::Vector3f::UnitY())); + rotateShipMat = q.toRotationMatrix(); + + if (!shipMoveLockActive) { + lockedCameraMat = Environment::shipMatrix; + shipMoveLockActive = true; + } + } + else { + Environment::shipVelocity = 0.0f; + } + } + else if (Environment::tapDownHold && !uiManager.isUiInteraction()) { float diffx = Environment::tapDownCurrentPos(0) - Environment::tapDownStartPos(0); float diffy = Environment::tapDownCurrentPos(1) - Environment::tapDownStartPos(1); - if (isDraggingShip) { - if (std::abs(diffy) > 5.0f || std::abs(diffx) > 5.0f) - { - float velocity = std::sqrt(diffx * diffx + diffy * diffy); - Environment::shipVelocity = velocity * static_cast(delta) / 100.0f; - - float angleY = std::atan2(-diffx, -diffy); - - Eigen::Quaternionf q(Eigen::AngleAxisf(angleY, Eigen::Vector3f::UnitY())); - rotateShipMat = q.toRotationMatrix(); - - if (!shipMoveLockActive) { - lockedCameraMat = Environment::shipMatrix; - shipMoveLockActive = true; - } - } - else { - Environment::shipVelocity = 0.0f; - } - } - else if (isDraggingCamera) { + // Только управление камерой через drag (вне зоны джойстика) + if (isDraggingCamera) { if (std::abs(diffy) > 5.0f || std::abs(diffx) > 5.0f) { float rotationPower = std::sqrt(diffx * diffx + diffy * diffy); - float deltaAlpha = rotationPower * static_cast(delta) * static_cast(M_PI) / 500000.f; + float deltaAlpha = rotationPower * static_cast(delta) * static_cast(M_PI) / 150000.f; Eigen::Vector3f rotationDirection(diffy, diffx, 0.0f); rotationDirection.normalize(); @@ -877,16 +892,10 @@ namespace ZL }(); if (!uiManager.isUiInteraction()) { - if (mx < 400 && my > 400) { - isDraggingShip = true; - isDraggingCamera = false; - - shipMoveLockActive = false; // новый drag -> новый lock - } - else { - isDraggingShip = false; - isDraggingCamera = true; - } + // Джойстик обрабатывает управление кораблём, поэтому + // любое нажатие вне UI элементов - это управление камерой + isDraggingShip = false; + isDraggingCamera = true; } } void Game::handleUp(int mx, int my) @@ -894,8 +903,19 @@ namespace ZL int uiX = mx; int uiY = Environment::height - my; + // Проверяем был ли активен джойстик до отпускания + auto joystick = uiManager.findJoystick("shipJoystick"); + bool wasJoystickActive = joystick && joystick->isActive; + uiManager.onMouseUp(uiX, uiY); + // Сбрасываем состояние если отпустили джойстик + if (wasJoystickActive) { + Environment::shipVelocity = 0.0f; + shipMoveLockActive = false; + rotateShipMat = Matrix3f::Identity(); + } + if (!uiManager.isUiInteraction()) { Environment::tapDownHold = false; Environment::shipVelocity = 0.0f; diff --git a/src/UiManager.cpp b/src/UiManager.cpp index 5187433..16473dc 100644 --- a/src/UiManager.cpp +++ b/src/UiManager.cpp @@ -167,6 +167,91 @@ namespace ZL { renderer.DisableVertexAttribArray(vTexCoordName); } + void UiJoystick::buildBaseMesh() { + baseMesh.data.PositionData.clear(); + baseMesh.data.TexCoordData.clear(); + + float x0 = centerX - baseRadius; + float y0 = centerY - baseRadius; + float x1 = centerX + baseRadius; + float y1 = centerY + baseRadius; + + baseMesh.data.PositionData.push_back({ x0, y0, 0 }); + baseMesh.data.TexCoordData.push_back({ 0, 0 }); + + baseMesh.data.PositionData.push_back({ x0, y1, 0 }); + baseMesh.data.TexCoordData.push_back({ 0, 1 }); + + baseMesh.data.PositionData.push_back({ x1, y1, 0 }); + baseMesh.data.TexCoordData.push_back({ 1, 1 }); + + baseMesh.data.PositionData.push_back({ x0, y0, 0 }); + baseMesh.data.TexCoordData.push_back({ 0, 0 }); + + baseMesh.data.PositionData.push_back({ x1, y1, 0 }); + baseMesh.data.TexCoordData.push_back({ 1, 1 }); + + baseMesh.data.PositionData.push_back({ x1, y0, 0 }); + baseMesh.data.TexCoordData.push_back({ 1, 0 }); + + baseMesh.RefreshVBO(); + } + + void UiJoystick::buildKnobMesh() { + knobMesh.data.PositionData.clear(); + knobMesh.data.TexCoordData.clear(); + + float cx = centerX + knobOffsetX; + float cy = centerY + knobOffsetY; + + float x0 = cx - knobRadius; + float y0 = cy - knobRadius; + float x1 = cx + knobRadius; + float y1 = cy + knobRadius; + + 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 UiJoystick::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 (texBase) { + glBindTexture(GL_TEXTURE_2D, texBase->getTexID()); + renderer.DrawVertexRenderStruct(baseMesh); + } + 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::string content; try { @@ -446,6 +531,57 @@ namespace ZL { return true; } + bool UiManager::addJoystick(const std::string& name, float centerX, float centerY, float baseRadius, float knobRadius, + Renderer& renderer, const std::string& zipFile, + const std::string& basePath, const std::string& knobPath) { + + auto j = std::make_shared(); + j->name = name; + j->centerX = centerX; + j->centerY = centerY; + j->baseRadius = baseRadius; + j->knobRadius = knobRadius; + j->knobOffsetX = 0; + j->knobOffsetY = 0; + j->isActive = false; + + try { + if (!basePath.empty()) { + auto data = CreateTextureDataFromPng(basePath.c_str(), zipFile.c_str()); + j->texBase = std::make_shared(data); + } + if (!knobPath.empty()) { + auto data = CreateTextureDataFromPng(knobPath.c_str(), zipFile.c_str()); + j->texKnob = std::make_shared(data); + } + } + catch (const std::exception& e) { + std::cerr << "UiManager: addJoystick failed to load textures: " << e.what() << std::endl; + return false; + } + + j->buildBaseMesh(); + j->buildKnobMesh(); + + joysticks.push_back(j); + return true; + } + + std::shared_ptr UiManager::findJoystick(const std::string& name) { + for (auto& j : joysticks) if (j->name == name) return j; + return nullptr; + } + + void UiManager::resetJoystick(const std::string& name) { + auto j = findJoystick(name); + if (j) { + j->knobOffsetX = 0; + j->knobOffsetY = 0; + j->isActive = false; + j->buildKnobMesh(); + } + } + bool UiManager::pushMenuFromFile(const std::string& path, Renderer& renderer, const std::string& zipFile) { MenuState prev; prev.root = root; @@ -526,6 +662,9 @@ namespace ZL { renderer.PushMatrix(); renderer.LoadIdentity(); + for (const auto& j : joysticks) { + j->draw(renderer); + } for (const auto& b : buttons) { b->draw(renderer); } @@ -724,6 +863,22 @@ namespace ZL { s->buildKnobMesh(); if (s->onValueChanged) s->onValueChanged(s->name, s->value); } + + if (pressedJoystick) { + auto j = pressedJoystick; + float dx = (float)x - j->centerX; + float dy = (float)y - j->centerY; + float dist = std::sqrt(dx * dx + dy * dy); + + if (dist > j->baseRadius) { + dx = dx / dist * j->baseRadius; + dy = dy / dist * j->baseRadius; + } + + j->knobOffsetX = dx; + j->knobOffsetY = dy; + j->buildKnobMesh(); + } } void UiManager::onMouseDown(int x, int y) { @@ -752,6 +907,27 @@ namespace ZL { break; } } + + for (auto& j : joysticks) { + if (j->contains((float)x, (float)y)) { + pressedJoystick = j; + j->isActive = true; + + float dx = (float)x - j->centerX; + float dy = (float)y - j->centerY; + float dist = std::sqrt(dx * dx + dy * dy); + + if (dist > j->baseRadius) { + dx = dx / dist * j->baseRadius; + dy = dy / dist * j->baseRadius; + } + + j->knobOffsetX = dx; + j->knobOffsetY = dy; + j->buildKnobMesh(); + break; + } + } } void UiManager::onMouseUp(int x, int y) { @@ -771,6 +947,14 @@ namespace ZL { if (pressedSlider) { pressedSlider.reset(); } + + if (pressedJoystick) { + pressedJoystick->knobOffsetX = 0; + pressedJoystick->knobOffsetY = 0; + pressedJoystick->isActive = false; + pressedJoystick->buildKnobMesh(); + pressedJoystick.reset(); + } } std::shared_ptr UiManager::findButton(const std::string& name) { diff --git a/src/UiManager.h b/src/UiManager.h index 5891566..b8edd0f 100644 --- a/src/UiManager.h +++ b/src/UiManager.h @@ -9,6 +9,8 @@ #include #include #include +#include +#include namespace ZL { @@ -71,6 +73,47 @@ namespace ZL { void draw(Renderer& renderer) const; }; + struct UiJoystick { + std::string name; + float centerX = 0; // центр джойстика (UI координаты) + float centerY = 0; + float baseRadius = 100; // радиус основания + float knobRadius = 40; // радиус ручки + + float knobOffsetX = 0; // смещение ручки от центра + float knobOffsetY = 0; + + bool isActive = false; // нажат ли джойстик + + std::shared_ptr texBase; // текстура основания + std::shared_ptr texKnob; // текстура ручки + + VertexRenderStruct baseMesh; + VertexRenderStruct knobMesh; + + // Возвращает направление -1..1 по каждой оси + float getDirectionX() const { return baseRadius > 0 ? knobOffsetX / baseRadius : 0; } + float getDirectionY() const { return baseRadius > 0 ? -knobOffsetY / baseRadius : 0; } + + // Возвращает силу 0..1 + float getMagnitude() const { + if (baseRadius <= 0) return 0; + float dist = std::sqrt(knobOffsetX * knobOffsetX + knobOffsetY * knobOffsetY); + float ratio = dist / baseRadius; + return ratio < 1.0f ? ratio : 1.0f; + } + + bool contains(float px, float py) const { + float dx = px - centerX; + float dy = py - centerY; + return (dx * dx + dy * dy) <= (baseRadius * baseRadius); + } + + void buildBaseMesh(); + void buildKnobMesh(); + void draw(Renderer& renderer) const; + }; + struct UiNode { std::string type; UiRect rect; @@ -108,7 +151,7 @@ namespace ZL { void onMouseUp(int x, int y); bool isUiInteraction() const { - return pressedButton != nullptr || pressedSlider != nullptr; + return pressedButton != nullptr || pressedSlider != nullptr || pressedJoystick != nullptr; } void stopAllAnimations() { @@ -135,6 +178,12 @@ namespace ZL { bool setSliderCallback(const std::string& name, std::function cb); bool setSliderValue(const std::string& name, float value); // programmatic set (clamped 0..1) + bool addJoystick(const std::string& name, float centerX, float centerY, float baseRadius, float knobRadius, + Renderer& renderer, const std::string& zipFile, + const std::string& basePath, const std::string& knobPath); + std::shared_ptr findJoystick(const std::string& name); + void resetJoystick(const std::string& name); // сбросить ручку в центр + bool pushMenuFromFile(const std::string& path, Renderer& renderer, const std::string& zipFile = ""); bool popMenu(); void clearMenuStack(); @@ -176,12 +225,14 @@ namespace ZL { std::shared_ptr root; std::vector> buttons; std::vector> sliders; + std::vector> joysticks; std::map, std::vector> nodeActiveAnims; std::map, std::function> animCallbacks; // key: (nodeName, animName) std::shared_ptr pressedButton; std::shared_ptr pressedSlider; + std::shared_ptr pressedJoystick; struct MenuState { std::shared_ptr root;