diff --git a/CMakeLists.txt b/CMakeLists.txt index 62c07ec00..10b0d03d1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -485,6 +485,11 @@ if(NOT NONET) endif() endif() +if (VIRTUAL_GAMEPAD) + list(APPEND libdevilutionx_SRCS + Source/controls/touch/gamepad.cpp) +endif() + set(BIN_TARGET devilutionx) if(NINTENDO_SWITCH) @@ -681,6 +686,7 @@ foreach( GPERF_HEAP_MAIN GPERF_HEAP_FIRST_GAME_ITERATION STREAM_ALL_AUDIO + VIRTUAL_GAMEPAD ) if(${def_name}) list(APPEND def_list ${def_name}) diff --git a/Source/controls/touch/gamepad.cpp b/Source/controls/touch/gamepad.cpp new file mode 100644 index 000000000..e7e6f7654 --- /dev/null +++ b/Source/controls/touch/gamepad.cpp @@ -0,0 +1,140 @@ +#include + +#include "controls/touch/gamepad.h" +#include "diablo.h" +#include "utils/display.h" +#include "utils/ui_fwd.h" + +namespace devilution { + +VirtualGamepad VirtualGamepadState; + +namespace { + +constexpr double Pi = 3.141592653589793; + +constexpr bool PointsUp(double angle) +{ + constexpr double UpAngle = Pi / 2; + constexpr double MinAngle = UpAngle - 3 * Pi / 8; + constexpr double MaxAngle = UpAngle + 3 * Pi / 8; + return MinAngle <= angle && angle <= MaxAngle; +} + +constexpr bool PointsDown(double angle) +{ + constexpr double DownAngle = -Pi / 2; + constexpr double MinAngle = DownAngle - 3 * Pi / 8; + constexpr double MaxAngle = DownAngle + 3 * Pi / 8; + return MinAngle <= angle && angle <= MaxAngle; +} + +constexpr bool PointsLeft(double angle) +{ + constexpr double MaxAngle = Pi - 3 * Pi / 8; + constexpr double MinAngle = -Pi + 3 * Pi / 8; + return !(MinAngle < angle && angle < MaxAngle); +} + +constexpr bool PointsRight(double angle) +{ + constexpr double MinAngle = -3 * Pi / 8; + constexpr double MaxAngle = 3 * Pi / 8; + return MinAngle <= angle && angle <= MaxAngle; +} + +} // namespace + +void InitializeVirtualGamepad() +{ + int screenPixels = std::min(gnScreenWidth, gnScreenHeight); + int inputMargin = screenPixels / 10; + int directionPadSize = screenPixels / 4; + int padButtonSize = round(1.1 * screenPixels / 10); + int padButtonSpacing = inputMargin / 3; + +#ifndef USE_SDL1 + float hdpi; + float vdpi; + int displayIndex = SDL_GetWindowDisplayIndex(ghMainWnd); + if (SDL_GetDisplayDPI(displayIndex, nullptr, &hdpi, &vdpi) == 0) { + int clientWidth; + int clientHeight; + SDL_GetWindowSize(ghMainWnd, &clientWidth, &clientHeight); + hdpi *= static_cast(gnScreenWidth) / clientWidth; + vdpi *= static_cast(gnScreenHeight) / clientHeight; + + float dpi = std::min(hdpi, vdpi); + inputMargin = round(0.25 * dpi); + directionPadSize = round(dpi); + padButtonSize = round(0.3 * dpi); + padButtonSpacing = round(0.1 * dpi); + } +#endif + + int padButtonAreaWidth = round(std::sqrt(2) * (padButtonSize + padButtonSpacing)); + + int padButtonRight = gnScreenWidth - inputMargin - padButtonSize / 2; + int padButtonLeft = padButtonRight - padButtonAreaWidth; + int padButtonBottom = gnScreenHeight - inputMargin - padButtonSize / 2; + int padButtonTop = padButtonBottom - padButtonAreaWidth; + + VirtualDirectionPad &directionPad = VirtualGamepadState.directionPad; + directionPad.area.position.x = inputMargin + directionPadSize / 2; + directionPad.area.position.y = gnScreenHeight - inputMargin - directionPadSize / 2; + directionPad.area.radius = directionPadSize / 2; + directionPad.position = directionPad.area.position; + + VirtualPadButton &primaryActionButton = VirtualGamepadState.primaryActionButton; + primaryActionButton.area.position.x = padButtonRight; + primaryActionButton.area.position.y = (padButtonTop + padButtonBottom) / 2; + primaryActionButton.area.radius = padButtonSize / 2; + + VirtualPadButton &secondaryActionButton = VirtualGamepadState.secondaryActionButton; + secondaryActionButton.area.position.x = (padButtonLeft + padButtonRight) / 2; + secondaryActionButton.area.position.y = padButtonTop; + secondaryActionButton.area.radius = padButtonSize / 2; + + VirtualPadButton &spellActionButton = VirtualGamepadState.spellActionButton; + spellActionButton.area.position.x = padButtonLeft; + spellActionButton.area.position.y = (padButtonTop + padButtonBottom) / 2; + spellActionButton.area.radius = padButtonSize / 2; + + VirtualPadButton &cancelButton = VirtualGamepadState.cancelButton; + cancelButton.area.position.x = (padButtonLeft + padButtonRight) / 2; + cancelButton.area.position.y = padButtonBottom; + cancelButton.area.radius = padButtonSize / 2; +} + +void VirtualDirectionPad::UpdatePosition(Point touchCoordinates) +{ + position = touchCoordinates; + + Displacement diff = position - area.position; + if (diff == Displacement { 0, 0 }) { + isUpPressed = false; + isDownPressed = false; + isLeftPressed = false; + isRightPressed = false; + return; + } + + if (!area.Contains(position)) { + int x = diff.deltaX; + int y = diff.deltaY; + double dist = sqrt(x * x + y * y); + x = round(x * area.radius / dist); + y = round(y * area.radius / dist); + position.x = area.position.x + x; + position.y = area.position.y + y; + } + + double angle = atan2(-diff.deltaY, diff.deltaX); + + isUpPressed = PointsUp(angle); + isDownPressed = PointsDown(angle); + isLeftPressed = PointsLeft(angle); + isRightPressed = PointsRight(angle); +} + +} // namespace devilution diff --git a/Source/controls/touch/gamepad.h b/Source/controls/touch/gamepad.h new file mode 100644 index 000000000..074ffd7cd --- /dev/null +++ b/Source/controls/touch/gamepad.h @@ -0,0 +1,65 @@ +#pragma once + +#if defined(VIRTUAL_GAMEPAD) && !defined(USE_SDL1) + +#include "controls/controller_buttons.h" +#include "engine/circle.hpp" +#include "engine/point.hpp" + +namespace devilution { + +struct VirtualDirectionPad { + Circle area; + Point position; + bool isUpPressed; + bool isDownPressed; + bool isLeftPressed; + bool isRightPressed; + + VirtualDirectionPad() + : area({ { 0, 0 }, 0 }) + , position({ 0, 0 }) + , isUpPressed(false) + , isDownPressed(false) + , isLeftPressed(false) + , isRightPressed(false) + { + } + + void UpdatePosition(Point touchCoordinates); +}; + +struct VirtualPadButton { + Circle area; + bool isHeld; + bool didStateChange; + + VirtualPadButton() + : area({ { 0, 0 }, 0 }) + , isHeld(false) + , didStateChange(false) + { + } +}; + +struct VirtualGamepad { + VirtualDirectionPad directionPad; + VirtualPadButton primaryActionButton; + VirtualPadButton secondaryActionButton; + VirtualPadButton spellActionButton; + VirtualPadButton cancelButton; + + VirtualGamepad() + { + } + + bool IsControllerButtonPressed(ControllerButton button) const; +}; + +void InitializeVirtualGamepad(); + +extern VirtualGamepad VirtualGamepadState; + +} // namespace devilution + +#endif diff --git a/Source/diablo.cpp b/Source/diablo.cpp index 60558ba71..9ad4bf71b 100644 --- a/Source/diablo.cpp +++ b/Source/diablo.cpp @@ -19,6 +19,7 @@ #endif #include "DiabloUI/diabloui.h" #include "controls/keymapper.hpp" +#include "controls/touch/gamepad.h" #include "diablo.h" #include "doom.h" #include "drlg_l1.h" @@ -956,6 +957,10 @@ void DiabloInit() strncpy(sgOptions.Chat.szHotKeyMsgs[i], _(QuickMessages[i].message), MAX_SEND_STR_LEN); } +#if defined(VIRTUAL_GAMEPAD) && !defined(USE_SDL1) + InitializeVirtualGamepad(); +#endif + UiInitialize(); UiSetSpawned(gbIsSpawn); was_ui_init = true; diff --git a/Source/engine/circle.hpp b/Source/engine/circle.hpp new file mode 100644 index 000000000..4e3a5bd59 --- /dev/null +++ b/Source/engine/circle.hpp @@ -0,0 +1,21 @@ +#pragma once + +#include "engine/displacement.hpp" +#include "engine/point.hpp" + +namespace devilution { + +struct Circle { + Point position; + int radius; + + constexpr bool Contains(Point point) const + { + Displacement diff = point - position; + int x = diff.deltaX; + int y = diff.deltaY; + return x * x + y * y < radius * radius; + } +}; + +} // namespace devilution