diff --git a/CMakeLists.txt b/CMakeLists.txt index 89cc81f6d6a..d4436088639 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2261,6 +2261,8 @@ if(CLIENT) components/statboard.h components/tooltips.cpp components/tooltips.h + components/touch_controls.cpp + components/touch_controls.h components/voting.cpp components/voting.h gameclient.cpp diff --git a/src/engine/client/input.cpp b/src/engine/client/input.cpp index dd5fe2ba688..8d42a441e55 100644 --- a/src/engine/client/input.cpp +++ b/src/engine/client/input.cpp @@ -304,6 +304,14 @@ const std::vector &CInput::TouchFingerStates() const return m_vTouchFingerStates; } +void CInput::ClearTouchDeltas() +{ + for(CTouchFingerState &TouchFingerState : m_vTouchFingerStates) + { + TouchFingerState.m_Delta = vec2(0.0f, 0.0f); + } +} + const char *CInput::GetClipboardText() { SDL_free(m_pClipboardText); @@ -351,10 +359,7 @@ void CInput::Clear() mem_zero(m_aInputState, sizeof(m_aInputState)); mem_zero(m_aInputCount, sizeof(m_aInputCount)); m_vInputEvents.clear(); - for(CTouchFingerState &TouchFingerState : m_vTouchFingerStates) - { - TouchFingerState.m_Delta = vec2(0.0f, 0.0f); - } + ClearTouchDeltas(); } float CInput::GetUpdateTime() const diff --git a/src/engine/client/input.h b/src/engine/client/input.h index e53ec3b4ce7..386c76f164c 100644 --- a/src/engine/client/input.h +++ b/src/engine/client/input.h @@ -149,6 +149,7 @@ class CInput : public IEngineInput bool NativeMousePressed(int Index) const override; const std::vector &TouchFingerStates() const override; + void ClearTouchDeltas() override; const char *GetClipboardText() override; void SetClipboardText(const char *pText) override; diff --git a/src/engine/input.h b/src/engine/input.h index efaec84f210..9b408090224 100644 --- a/src/engine/input.h +++ b/src/engine/input.h @@ -146,6 +146,12 @@ class IInput : public IInterface * @return vector of all touch finger states */ virtual const std::vector &TouchFingerStates() const = 0; + /** + * Must be called after the touch finger states have been used during the client update to ensure that + * touch deltas are only accumulated until the next update. If the touch states are only using during + * rendering, i.e. for user interfaces, then this is called automatically by calling @link Clear @endlink. + */ + virtual void ClearTouchDeltas() = 0; // clipboard virtual const char *GetClipboardText() = 0; diff --git a/src/engine/shared/config_variables.h b/src/engine/shared/config_variables.h index f9df0aad14d..f161ba686fb 100644 --- a/src/engine/shared/config_variables.h +++ b/src/engine/shared/config_variables.h @@ -22,6 +22,12 @@ MACRO_CONFIG_INT(ClAntiPingSmooth, cl_antiping_smooth, 0, 0, 1, CFGFLAG_CLIENT | MACRO_CONFIG_INT(ClAntiPingGunfire, cl_antiping_gunfire, 1, 0, 1, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Predict gunfire and show predicted weapon physics (with cl_antiping_grenade 1 and cl_antiping_weapons 1)") MACRO_CONFIG_INT(ClPredictionMargin, cl_prediction_margin, 10, 1, 300, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Prediction margin in ms (adds latency, can reduce lag from ping jumps)") MACRO_CONFIG_INT(ClSubTickAiming, cl_sub_tick_aiming, 0, 0, 1, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Send aiming data at sub-tick accuracy") +#if defined(CONF_PLATFORM_ANDROID) +MACRO_CONFIG_INT(ClTouchControls, cl_touch_controls, 1, 0, 1, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Enable ingame touch controls") +#else +MACRO_CONFIG_INT(ClTouchControls, cl_touch_controls, 0, 0, 1, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Enable ingame touch controls") +#endif +MACRO_CONFIG_INT(ClTouchControlsJoystick, cl_touch_controls_joystick, 0, 0, 1, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Use virtual joystick for ingame touch controls") MACRO_CONFIG_INT(ClNameplates, cl_nameplates, 1, 0, 1, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Show name plates") MACRO_CONFIG_INT(ClAfkEmote, cl_afk_emote, 1, 0, 1, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Show zzz emote next to afk players") diff --git a/src/game/client/component.h b/src/game/client/component.h index 639c8b140ad..9d14615a058 100644 --- a/src/game/client/component.h +++ b/src/game/client/component.h @@ -212,6 +212,14 @@ class CComponent * @param Event The input event. */ virtual bool OnInput(const IInput::CEvent &Event) { return false; } + /** + * Called with all current touch finger states. + * + * @param vTouchFingerStates The touch finger states to be handled. + * + * @return `true` if the component used the touch events, `false` otherwise + */ + virtual bool OnTouchState(const std::vector &vTouchFingerStates) { return false; } }; #endif diff --git a/src/game/client/components/controls.h b/src/game/client/components/controls.h index ce6bc664dd1..11eecdde03d 100644 --- a/src/game/client/components/controls.h +++ b/src/game/client/components/controls.h @@ -12,10 +12,10 @@ class CControls : public CComponent { +public: float GetMinMouseDistance() const; float GetMaxMouseDistance() const; -public: vec2 m_aMousePos[NUM_DUMMIES]; vec2 m_aMousePosOnAction[NUM_DUMMIES]; vec2 m_aTargetPos[NUM_DUMMIES]; diff --git a/src/game/client/components/menus.h b/src/game/client/components/menus.h index 7bd0e7303a6..7df7f311e63 100644 --- a/src/game/client/components/menus.h +++ b/src/game/client/components/menus.h @@ -592,7 +592,6 @@ class CMenus : public CComponent void RenderSettingsCustom(CUIRect MainView); void SetNeedSendInfo(); - void SetActive(bool Active); void UpdateColors(); IGraphics::CTextureHandle m_TextureBlob; @@ -612,6 +611,8 @@ class CMenus : public CComponent bool IsInit() { return m_IsInit; } bool IsActive() const { return m_MenuActive; } + void SetActive(bool Active); + void KillServer(); virtual void OnInit() override; diff --git a/src/game/client/components/menus_ingame.cpp b/src/game/client/components/menus_ingame.cpp index d9bb98b097a..263f6ad3511 100644 --- a/src/game/client/components/menus_ingame.cpp +++ b/src/game/client/components/menus_ingame.cpp @@ -42,19 +42,15 @@ void CMenus::RenderGame(CUIRect MainView) { CUIRect Button, ButtonBar, ButtonBar2; bool ShowDDRaceButtons = MainView.w > 855.0f; - MainView.HSplitTop(45.0f, &ButtonBar, &MainView); + MainView.HSplitTop(45.0f + (g_Config.m_ClTouchControls ? 35.0f : 0.0f), &ButtonBar, &MainView); ButtonBar.Draw(ms_ColorTabbarActive, IGraphics::CORNER_B, 10.0f); - - // button bar - ButtonBar.HSplitTop(10.0f, 0, &ButtonBar); - ButtonBar.HSplitTop(25.0f, &ButtonBar, 0); - ButtonBar.VMargin(10.0f, &ButtonBar); - - ButtonBar.HSplitTop(30.0f, 0, &ButtonBar2); - ButtonBar2.HSplitTop(25.0f, &ButtonBar2, 0); + ButtonBar.Margin(10.0f, &ButtonBar); + if(g_Config.m_ClTouchControls) + { + ButtonBar.HSplitMid(&ButtonBar, &ButtonBar2, 10.0f); + } ButtonBar.VSplitRight(120.0f, &ButtonBar, &Button); - static CButtonContainer s_DisconnectButton; if(DoButton_Menu(&s_DisconnectButton, Localize("Disconnect"), 0, &Button)) { @@ -69,7 +65,7 @@ void CMenus::RenderGame(CUIRect MainView) } } - ButtonBar.VSplitRight(5.0f, &ButtonBar, 0); + ButtonBar.VSplitRight(5.0f, &ButtonBar, nullptr); ButtonBar.VSplitRight(170.0f, &ButtonBar, &Button); bool DummyConnecting = Client()->DummyConnecting(); @@ -102,9 +98,8 @@ void CMenus::RenderGame(CUIRect MainView) } } - ButtonBar.VSplitRight(5.0f, &ButtonBar, 0); + ButtonBar.VSplitRight(5.0f, &ButtonBar, nullptr); ButtonBar.VSplitRight(140.0f, &ButtonBar, &Button); - static CButtonContainer s_DemoButton; const bool Recording = DemoRecorder(RECORDER_MANUAL)->IsRecording(); if(DoButton_Menu(&s_DemoButton, Recording ? Localize("Stop record") : Localize("Record demo"), 0, &Button)) @@ -129,7 +124,6 @@ void CMenus::RenderGame(CUIRect MainView) if(m_pClient->m_Snap.m_pLocalInfo->m_Team != TEAM_SPECTATORS) { - ButtonBar.VSplitLeft(5.0f, 0, &ButtonBar); ButtonBar.VSplitLeft(120.0f, &Button, &ButtonBar); if(!DummyConnecting && DoButton_Menu(&s_SpectateButton, Localize("Spectate"), 0, &Button)) { @@ -145,7 +139,7 @@ void CMenus::RenderGame(CUIRect MainView) { if(m_pClient->m_Snap.m_pLocalInfo->m_Team != TEAM_RED) { - ButtonBar.VSplitLeft(5.0f, 0, &ButtonBar); + ButtonBar.VSplitLeft(5.0f, nullptr, &ButtonBar); ButtonBar.VSplitLeft(120.0f, &Button, &ButtonBar); static CButtonContainer s_JoinRedButton; if(!DummyConnecting && DoButton_Menu(&s_JoinRedButton, Localize("Join red"), 0, &Button)) @@ -157,7 +151,7 @@ void CMenus::RenderGame(CUIRect MainView) if(m_pClient->m_Snap.m_pLocalInfo->m_Team != TEAM_BLUE) { - ButtonBar.VSplitLeft(5.0f, 0, &ButtonBar); + ButtonBar.VSplitLeft(5.0f, nullptr, &ButtonBar); ButtonBar.VSplitLeft(120.0f, &Button, &ButtonBar); static CButtonContainer s_JoinBlueButton; if(!DummyConnecting && DoButton_Menu(&s_JoinBlueButton, Localize("Join blue"), 0, &Button)) @@ -171,7 +165,7 @@ void CMenus::RenderGame(CUIRect MainView) { if(m_pClient->m_Snap.m_pLocalInfo->m_Team != 0) { - ButtonBar.VSplitLeft(5.0f, 0, &ButtonBar); + ButtonBar.VSplitLeft(5.0f, nullptr, &ButtonBar); ButtonBar.VSplitLeft(120.0f, &Button, &ButtonBar); if(!DummyConnecting && DoButton_Menu(&s_SpectateButton, Localize("Join game"), 0, &Button)) { @@ -183,7 +177,7 @@ void CMenus::RenderGame(CUIRect MainView) if(m_pClient->m_Snap.m_pLocalInfo->m_Team != TEAM_SPECTATORS && ShowDDRaceButtons) { - ButtonBar.VSplitLeft(5.0f, 0, &ButtonBar); + ButtonBar.VSplitLeft(5.0f, nullptr, &ButtonBar); ButtonBar.VSplitLeft(65.0f, &Button, &ButtonBar); static CButtonContainer s_KillButton; @@ -199,17 +193,58 @@ void CMenus::RenderGame(CUIRect MainView) { if(m_pClient->m_Snap.m_pLocalInfo->m_Team != TEAM_SPECTATORS || Paused || Spec) { - ButtonBar.VSplitLeft(5.0f, 0, &ButtonBar); + ButtonBar.VSplitLeft(5.0f, nullptr, &ButtonBar); ButtonBar.VSplitLeft((!Paused && !Spec) ? 65.0f : 120.0f, &Button, &ButtonBar); static CButtonContainer s_PauseButton; if(DoButton_Menu(&s_PauseButton, (!Paused && !Spec) ? Localize("Pause") : Localize("Join game"), 0, &Button)) { - m_pClient->Console()->ExecuteLine("say /pause"); + Console()->ExecuteLine("say /pause"); SetActive(false); } } } + + if(!g_Config.m_ClTouchControls) + return; + + ButtonBar2.VSplitLeft(170.0f, &Button, &ButtonBar2); + static char s_VirtualJoystickCheckbox; + if(DoButton_CheckBox(&s_VirtualJoystickCheckbox, Localize("Virtual joystick"), g_Config.m_ClTouchControlsJoystick, &Button)) + { + g_Config.m_ClTouchControlsJoystick = !g_Config.m_ClTouchControlsJoystick; + } + + ButtonBar2.VSplitLeft(5.0f, nullptr, &ButtonBar2); + ButtonBar2.VSplitLeft(170.0f, &Button, &ButtonBar2); + static char s_ShowEntitiesCheckbox; + if(DoButton_CheckBox(&s_ShowEntitiesCheckbox, Localize("Show entities"), g_Config.m_ClOverlayEntities, &Button)) + { + Console()->ExecuteLine("toggle cl_overlay_entities 0 100"); + } + + ButtonBar2.VSplitRight(80.0f, &ButtonBar2, &Button); + static CButtonContainer s_CloseButton; + if(DoButton_Menu(&s_CloseButton, Localize("Close"), 0, &Button)) + { + SetActive(false); + } + + ButtonBar2.VSplitRight(5.0f, &ButtonBar2, nullptr); + ButtonBar2.VSplitRight(160.0f, &ButtonBar2, &Button); + static CButtonContainer s_RemoveConsoleButton; + if(DoButton_Menu(&s_RemoveConsoleButton, Localize("Remote console"), 0, &Button)) + { + Console()->ExecuteLine("toggle_remote_console"); + } + + ButtonBar2.VSplitRight(5.0f, &ButtonBar2, nullptr); + ButtonBar2.VSplitRight(160.0f, &ButtonBar2, &Button); + static CButtonContainer s_LocalConsoleButton; + if(DoButton_Menu(&s_LocalConsoleButton, Localize("Local console"), 0, &Button)) + { + Console()->ExecuteLine("toggle_local_console"); + } } void CMenus::PopupConfirmDisconnect() diff --git a/src/game/client/components/touch_controls.cpp b/src/game/client/components/touch_controls.cpp new file mode 100644 index 00000000000..0906054705c --- /dev/null +++ b/src/game/client/components/touch_controls.cpp @@ -0,0 +1,598 @@ +#include "touch_controls.h" + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +static constexpr const char *ACTION_NAMES[] = {"Fire", "Hook"}; +static constexpr const char *ACTION_SWAP_NAMES[] = {"Active: Fire", "Active: Hook"}; +static constexpr const char *ACTION_COMMANDS[] = {"+fire", "+hook"}; + +CTouchControls::CTouchButton::CTouchButton(CTouchControls *pTouchControls) : + m_pTouchControls(pTouchControls), + m_BackgroundCorners(IGraphics::CORNER_ALL), + m_BackgroundRounding(10.0f), + m_Active(false) +{ +} + +void CTouchControls::CTouchButton::SetActive(const IInput::CTouchFingerState &FingerState) +{ + const vec2 Position = (FingerState.m_Position - m_UnitRect.TopLeft()) / m_UnitRect.Size(); + const vec2 Delta = FingerState.m_Delta / m_UnitRect.Size(); + if(!m_Active) + { + m_Active = true; + m_ActivePosition = Position; + m_AccumulatedDelta = Delta; + m_ActivationStartTime = m_pTouchControls->Client()->GlobalTime(); + m_Finger = FingerState.m_Finger; + m_ActivateFunction(*this); + } + else if(m_Finger == FingerState.m_Finger) + { + m_ActivePosition = Position; + m_AccumulatedDelta += Delta; + if(m_MovementFunction) + { + m_MovementFunction(*this); + } + } +} + +// TODO: Optimization: Use text and quad containers for rendering +void CTouchControls::CTouchButton::Render() +{ + const ColorRGBA ButtonColor = m_Active ? ColorRGBA(0.2f, 0.2f, 0.2f, 0.25f) : ColorRGBA(0.0f, 0.0f, 0.0f, 0.25f); + m_ScreenRect.Draw(ButtonColor, m_BackgroundCorners, m_BackgroundRounding); + + CUIRect Label; + m_ScreenRect.Margin(10.0f, &Label); + SLabelProperties LabelProps; + LabelProps.m_MaxWidth = Label.w; + m_pTouchControls->Ui()->DoLabel(&Label, m_LabelFunction(), 20.0f, TEXTALIGN_MC, LabelProps); +} + +void CTouchControls::CTouchButton::SetInactive() +{ + if(m_Active) + { + m_Active = false; + m_DeactivateFunction(*this); + } +} + +static CUIRect ScaleRect(CUIRect Rect, vec2 Factor) +{ + Rect.x *= Factor.x; + Rect.y *= Factor.y; + Rect.w *= Factor.x; + Rect.h *= Factor.y; + return Rect; +} + +void CTouchControls::InitButtons(vec2 ScreenSize) +{ + const float LongTouchThreshold = 0.5f; + + const auto &&AlwaysVisibleFunction = []() { + return true; + }; + const auto &&IngameVisibleFunction = [&]() { + return !GameClient()->m_Snap.m_SpecInfo.m_Active; + }; + const auto &&ExtraButtonVisibleFunction = [&]() { + return m_ExtraButtonsActive; + }; + const auto &&ExtraButtonIngameVisibleFunction = [&]() { + return m_ExtraButtonsActive && !GameClient()->m_Snap.m_SpecInfo.m_Active; + }; + const auto &&ExtraButtonSpectateVisibleFunction = [&]() { + return m_ExtraButtonsActive && GameClient()->m_Snap.m_SpecInfo.m_Active; + }; + const auto &&ZoomVisibleFunction = [&]() { + return m_ExtraButtonsActive && GameClient()->m_Camera.ZoomAllowed(); + }; + const auto &&VoteVisibleFunction = [&]() { + return m_ExtraButtonsActive && GameClient()->m_Voting.IsActive(); + }; + + // TODO: Use icons/images instead of text labels where possible, make remaining texts localizable + + const vec2 ButtonGridSize = vec2(1.0f, 1.0f) / 120.0f; + + CTouchButton LeftButton(this); + LeftButton.m_UnitRect = ScaleRect(CUIRect{0, 100, 24, 20}, ButtonGridSize); + LeftButton.m_ScreenRect = ScaleRect(LeftButton.m_UnitRect, ScreenSize); + LeftButton.m_LabelFunction = []() { return "Left"; }; + LeftButton.m_ActivateFunction = [&](CTouchButton &TouchButton) { Console()->ExecuteLineStroked(1, "+left"); }; + LeftButton.m_DeactivateFunction = [&](CTouchButton &TouchButton) { Console()->ExecuteLineStroked(0, "+left"); }; + LeftButton.m_IsVisibleFunction = IngameVisibleFunction; + LeftButton.m_BackgroundCorners = IGraphics::CORNER_NONE; + m_vTouchButtons.emplace_back(std::move(LeftButton)); + + CTouchButton RightButton(this); + RightButton.m_UnitRect = ScaleRect(CUIRect{24, 100, 24, 20}, ButtonGridSize); + RightButton.m_ScreenRect = ScaleRect(RightButton.m_UnitRect, ScreenSize); + RightButton.m_LabelFunction = []() { return "Right"; }; + RightButton.m_ActivateFunction = [&](CTouchButton &TouchButton) { Console()->ExecuteLineStroked(1, "+right"); }; + RightButton.m_DeactivateFunction = [&](CTouchButton &TouchButton) { Console()->ExecuteLineStroked(0, "+right"); }; + RightButton.m_IsVisibleFunction = IngameVisibleFunction; + RightButton.m_BackgroundCorners = IGraphics::CORNER_TR; + m_vTouchButtons.emplace_back(std::move(RightButton)); + + CTouchButton JumpButton(this); + JumpButton.m_UnitRect = ScaleRect(CUIRect{12, 80, 24, 20}, ButtonGridSize); + JumpButton.m_ScreenRect = ScaleRect(JumpButton.m_UnitRect, ScreenSize); + JumpButton.m_LabelFunction = []() { return "Jump"; }; + JumpButton.m_ActivateFunction = [&](CTouchButton &TouchButton) { Console()->ExecuteLineStroked(1, "+jump"); }; + JumpButton.m_DeactivateFunction = [&](CTouchButton &TouchButton) { Console()->ExecuteLineStroked(0, "+jump"); }; + JumpButton.m_IsVisibleFunction = IngameVisibleFunction; + JumpButton.m_BackgroundCorners = IGraphics::CORNER_T; + m_vTouchButtons.emplace_back(std::move(JumpButton)); + + // Switch weapon button: + // - swipe left/right to switch to previous/next weapon + // - tap quickly to switch to next weapon + CTouchButton SwitchWeaponButton(this); + SwitchWeaponButton.m_UnitRect = ScaleRect(CUIRect{14, 2, 20, 10}, ButtonGridSize); + SwitchWeaponButton.m_ScreenRect = ScaleRect(SwitchWeaponButton.m_UnitRect, ScreenSize); + SwitchWeaponButton.m_LabelFunction = []() { return "← Switch weapon →"; }; + SwitchWeaponButton.m_ActivateFunction = [](CTouchButton &TouchButton) {}; + SwitchWeaponButton.m_DeactivateFunction = [&](CTouchButton &TouchButton) { + if(TouchButton.m_AccumulatedDelta.x < -0.25f) + { + Console()->ExecuteLine("+prevweapon"); + } + else if(TouchButton.m_AccumulatedDelta.x > 0.25f || + Client()->GlobalTime() - TouchButton.m_ActivationStartTime < 0.25f) + { + Console()->ExecuteLine("+nextweapon"); + } + }; + SwitchWeaponButton.m_IsVisibleFunction = IngameVisibleFunction; + m_vTouchButtons.emplace_back(std::move(SwitchWeaponButton)); + + // TODO: Decide whether the switch weapon button above works well. Alternatively use two separate prev and next buttons: + /* + CTouchButton PrevWeaponButton(this); + PrevWeaponButton.m_UnitRect = ScaleRect(CUIRect{14, 2, 10, 10}, ButtonGridSize); + PrevWeaponButton.m_ScreenRect = ScaleRect(PrevWeaponButton.m_UnitRect, ScreenSize); + PrevWeaponButton.m_LabelFunction = []() { return "Prev. weapon"; }; + PrevWeaponButton.m_ActivateFunction = [&](CTouchButton &TouchButton) { Console()->ExecuteLineStroked(1, "+prevweapon"); }; + PrevWeaponButton.m_DeactivateFunction = [&](CTouchButton &TouchButton) { Console()->ExecuteLineStroked(0, "+prevweapon"); }; + PrevWeaponButton.m_IsVisibleFunction = IngameVisibleFunction; + PrevWeaponButton.m_BackgroundCorners = IGraphics::CORNER_L; + m_vTouchButtons.emplace_back(std::move(PrevWeaponButton)); + + CTouchButton NextWeaponButton(this); + NextWeaponButton.m_UnitRect = ScaleRect(CUIRect{24, 2, 10, 10}, ButtonGridSize); + NextWeaponButton.m_ScreenRect = ScaleRect(NextWeaponButton.m_UnitRect, ScreenSize); + NextWeaponButton.m_LabelFunction = []() { return "Next weapon"; }; + NextWeaponButton.m_ActivateFunction = [&](CTouchButton &TouchButton) { Console()->ExecuteLineStroked(1, "+nextweapon"); }; + NextWeaponButton.m_DeactivateFunction = [&](CTouchButton &TouchButton) { Console()->ExecuteLineStroked(0, "+nextweapon"); }; + NextWeaponButton.m_IsVisibleFunction = IngameVisibleFunction; + NextWeaponButton.m_BackgroundCorners = IGraphics::CORNER_R; + m_vTouchButtons.emplace_back(std::move(NextWeaponButton)); + */ + + // Menu button: + // - Short press: show/hide additional buttons + // - Long press: open ingame menu + CTouchButton MenuButton(this); + MenuButton.m_UnitRect = ScaleRect(CUIRect{2, 2, 10, 10}, ButtonGridSize); + MenuButton.m_ScreenRect = ScaleRect(MenuButton.m_UnitRect, ScreenSize); + MenuButton.m_LabelFunction = []() { return "☰"; }; + MenuButton.m_ActivateFunction = [](CTouchButton &TouchButton) {}; + MenuButton.m_DeactivateFunction = [&](CTouchButton &TouchButton) { + if(Client()->GlobalTime() - TouchButton.m_ActivationStartTime >= LongTouchThreshold) + { + GameClient()->m_Menus.SetActive(true); + } + else + { + m_ExtraButtonsActive = !m_ExtraButtonsActive; + } + }; + MenuButton.m_IsVisibleFunction = AlwaysVisibleFunction; + m_vTouchButtons.emplace_back(std::move(MenuButton)); + + // TODO: Eventually support standard gestures to zoom (only active in spectator mode to avoid interference while playing) + CTouchButton ZoomOutButton(this); + ZoomOutButton.m_UnitRect = ScaleRect(CUIRect{36, 2, 10, 10}, ButtonGridSize); + ZoomOutButton.m_ScreenRect = ScaleRect(ZoomOutButton.m_UnitRect, ScreenSize); + ZoomOutButton.m_LabelFunction = []() { return "Zoom out"; }; + ZoomOutButton.m_ActivateFunction = [](CTouchButton &TouchButton) {}; + ZoomOutButton.m_DeactivateFunction = [&](CTouchButton &TouchButton) { Console()->ExecuteLine("zoom-"); }; + ZoomOutButton.m_IsVisibleFunction = ZoomVisibleFunction; + ZoomOutButton.m_BackgroundCorners = IGraphics::CORNER_L; + m_vTouchButtons.emplace_back(std::move(ZoomOutButton)); + + CTouchButton ZoomDefaultButton(this); + ZoomDefaultButton.m_UnitRect = ScaleRect(CUIRect{46, 2, 10, 10}, ButtonGridSize); + ZoomDefaultButton.m_ScreenRect = ScaleRect(ZoomDefaultButton.m_UnitRect, ScreenSize); + ZoomDefaultButton.m_LabelFunction = []() { return "Default zoom"; }; + ZoomDefaultButton.m_ActivateFunction = [](CTouchButton &TouchButton) {}; + ZoomDefaultButton.m_DeactivateFunction = [&](CTouchButton &TouchButton) { Console()->ExecuteLine("zoom"); }; + ZoomDefaultButton.m_IsVisibleFunction = ZoomVisibleFunction; + ZoomDefaultButton.m_BackgroundCorners = IGraphics::CORNER_NONE; + m_vTouchButtons.emplace_back(std::move(ZoomDefaultButton)); + + CTouchButton ZoomInButton(this); + ZoomInButton.m_UnitRect = ScaleRect(CUIRect{56, 2, 10, 10}, ButtonGridSize); + ZoomInButton.m_ScreenRect = ScaleRect(ZoomInButton.m_UnitRect, ScreenSize); + ZoomInButton.m_LabelFunction = []() { return "Zoom in"; }; + ZoomInButton.m_ActivateFunction = [](CTouchButton &TouchButton) {}; + ZoomInButton.m_DeactivateFunction = [&](CTouchButton &TouchButton) { Console()->ExecuteLine("zoom+"); }; + ZoomInButton.m_IsVisibleFunction = ZoomVisibleFunction; + ZoomInButton.m_BackgroundCorners = IGraphics::CORNER_R; + m_vTouchButtons.emplace_back(std::move(ZoomInButton)); + + CTouchButton ScoreboardButton(this); + ScoreboardButton.m_UnitRect = ScaleRect(CUIRect{2, 16, 10, 8}, ButtonGridSize); + ScoreboardButton.m_ScreenRect = ScaleRect(ScoreboardButton.m_UnitRect, ScreenSize); + ScoreboardButton.m_LabelFunction = []() { return "Scoreboard"; }; + ScoreboardButton.m_ActivateFunction = [&](CTouchButton &TouchButton) { Console()->ExecuteLineStroked(1, "+scoreboard"); }; + ScoreboardButton.m_DeactivateFunction = [&](CTouchButton &TouchButton) { Console()->ExecuteLineStroked(0, "+scoreboard"); }; + ScoreboardButton.m_IsVisibleFunction = ExtraButtonVisibleFunction; + m_vTouchButtons.emplace_back(std::move(ScoreboardButton)); + + // TODO: Activate emoticon UI (button would have to toggle it, instead of activating it while holding like the scoreboard) + // TODO: Add touch support to emoticon UI + CTouchButton EmoticonButton(this); + EmoticonButton.m_UnitRect = ScaleRect(CUIRect{14, 16, 10, 8}, ButtonGridSize); // Emoticon and spectate buttons have same position + EmoticonButton.m_ScreenRect = ScaleRect(EmoticonButton.m_UnitRect, ScreenSize); + EmoticonButton.m_LabelFunction = []() { return "Emoticon\n(NOT IMPL.)"; }; + EmoticonButton.m_ActivateFunction = [](CTouchButton &TouchButton) {}; + EmoticonButton.m_DeactivateFunction = [](CTouchButton &TouchButton) {}; + EmoticonButton.m_IsVisibleFunction = ExtraButtonIngameVisibleFunction; + m_vTouchButtons.emplace_back(std::move(EmoticonButton)); + + // TODO: Activate emoticon UI (button would have to toggle it, instead of activating it while holding like the scoreboard) + // TODO: Add touch support to spectate UI + CTouchButton SpectateButton(this); + SpectateButton.m_UnitRect = ScaleRect(CUIRect{14, 16, 10, 8}, ButtonGridSize); + SpectateButton.m_ScreenRect = ScaleRect(SpectateButton.m_UnitRect, ScreenSize); + SpectateButton.m_LabelFunction = []() { return "Spectate\n(NOT IMPL.)"; }; // Emoticon and spectate buttons have same position + SpectateButton.m_ActivateFunction = [](CTouchButton &TouchButton) {}; + SpectateButton.m_DeactivateFunction = [](CTouchButton &TouchButton) {}; + SpectateButton.m_IsVisibleFunction = ExtraButtonSpectateVisibleFunction; + m_vTouchButtons.emplace_back(std::move(SpectateButton)); + + CTouchButton AllChatButton(this); + AllChatButton.m_UnitRect = ScaleRect(CUIRect{26, 16, 10, 8}, ButtonGridSize); + AllChatButton.m_ScreenRect = ScaleRect(AllChatButton.m_UnitRect, ScreenSize); + AllChatButton.m_LabelFunction = []() { return "Chat"; }; + AllChatButton.m_ActivateFunction = [](CTouchButton &TouchButton) {}; + AllChatButton.m_DeactivateFunction = [&](CTouchButton &TouchButton) { Console()->ExecuteLine("+show_chat; chat all"); }; + AllChatButton.m_IsVisibleFunction = ExtraButtonVisibleFunction; + m_vTouchButtons.emplace_back(std::move(AllChatButton)); + + CTouchButton TeamChatButton(this); + TeamChatButton.m_UnitRect = ScaleRect(CUIRect{38, 16, 10, 8}, ButtonGridSize); + TeamChatButton.m_ScreenRect = ScaleRect(TeamChatButton.m_UnitRect, ScreenSize); + TeamChatButton.m_LabelFunction = []() { return "Team chat"; }; + TeamChatButton.m_ActivateFunction = [](CTouchButton &TouchButton) {}; + TeamChatButton.m_DeactivateFunction = [&](CTouchButton &TouchButton) { Console()->ExecuteLine("+show_chat; chat team"); }; + TeamChatButton.m_IsVisibleFunction = ExtraButtonVisibleFunction; + m_vTouchButtons.emplace_back(std::move(TeamChatButton)); + + CTouchButton VoteYesButton(this); + VoteYesButton.m_UnitRect = ScaleRect(CUIRect{2, 40, 10, 8}, ButtonGridSize); + VoteYesButton.m_ScreenRect = ScaleRect(VoteYesButton.m_UnitRect, ScreenSize); + VoteYesButton.m_LabelFunction = []() { return "Vote yes"; }; + VoteYesButton.m_ActivateFunction = [](CTouchButton &TouchButton) {}; + VoteYesButton.m_DeactivateFunction = [&](CTouchButton &TouchButton) { Console()->ExecuteLine("vote yes"); }; + VoteYesButton.m_IsVisibleFunction = VoteVisibleFunction; + VoteYesButton.m_BackgroundCorners = IGraphics::CORNER_ALL; + m_vTouchButtons.emplace_back(std::move(VoteYesButton)); + + CTouchButton VoteNoButton(this); + VoteNoButton.m_UnitRect = ScaleRect(CUIRect{14, 40, 10, 8}, ButtonGridSize); + VoteNoButton.m_ScreenRect = ScaleRect(VoteNoButton.m_UnitRect, ScreenSize); + VoteNoButton.m_LabelFunction = []() { return "Vote no"; }; + VoteNoButton.m_ActivateFunction = [](CTouchButton &TouchButton) {}; + VoteNoButton.m_DeactivateFunction = [&](CTouchButton &TouchButton) { Console()->ExecuteLine("vote no"); }; + VoteNoButton.m_IsVisibleFunction = VoteVisibleFunction; + VoteNoButton.m_BackgroundCorners = IGraphics::CORNER_ALL; + m_vTouchButtons.emplace_back(std::move(VoteNoButton)); + + CTouchButton ToggleDummyButton(this); + ToggleDummyButton.m_UnitRect = ScaleRect(CUIRect{92, 2, 12, 12}, ButtonGridSize); + ToggleDummyButton.m_ScreenRect = ScaleRect(ToggleDummyButton.m_UnitRect, ScreenSize); + ToggleDummyButton.m_LabelFunction = [&]() { + if(Client()->DummyConnecting()) + { + return "Dummy connecting…"; + } + else if(!Client()->DummyConnected()) + { + return "Connect dummy"; + } + else + { + return "Toggle dummy"; + } + }; + ToggleDummyButton.m_ActivateFunction = [](CTouchButton &TouchButton) {}; + ToggleDummyButton.m_DeactivateFunction = [&](CTouchButton &TouchButton) { + if(Client()->DummyConnecting()) + { + return; + } + else if(Client()->DummyConnected()) + { + g_Config.m_ClDummy = !g_Config.m_ClDummy; + } + else + { + Client()->DummyConnect(); + } + }; + ToggleDummyButton.m_IsVisibleFunction = [&]() { + return Client()->DummyAllowed() && (Client()->DummyConnected() || m_ExtraButtonsActive); + }; + m_vTouchButtons.emplace_back(std::move(ToggleDummyButton)); + + // Action button: + // - If joystick is currently active with one action: activate the other action + // - Else: swap active action + CTouchButton ActionButton(this); + ActionButton.m_UnitRect = ScaleRect(CUIRect{106, 2, 12, 12}, ButtonGridSize); + ActionButton.m_ScreenRect = ScaleRect(ActionButton.m_UnitRect, ScreenSize); + ActionButton.m_LabelFunction = [&]() { + if(m_JoystickActionSecondary != NUM_ACTIONS) + { + return ACTION_NAMES[m_JoystickActionSecondary]; + } + else if(m_JoystickActionPrimary != NUM_ACTIONS) + { + return ACTION_NAMES[(m_JoystickActionPrimary + 1) % NUM_ACTIONS]; + } + return ACTION_SWAP_NAMES[m_ActionSelected]; + }; + ActionButton.m_ActivateFunction = [&](CTouchButton &TouchButton) { + if(m_JoystickActionPrimary != NUM_ACTIONS) + { + m_JoystickActionSecondary = (m_JoystickActionPrimary + 1) % NUM_ACTIONS; + Console()->ExecuteLineStroked(1, ACTION_COMMANDS[m_JoystickActionSecondary]); + } + else + { + m_ActionSelected = (m_ActionSelected + 1) % NUM_ACTIONS; + } + }; + ActionButton.m_DeactivateFunction = [&](CTouchButton &TouchButton) { + if(m_JoystickActionSecondary != NUM_ACTIONS) + { + Console()->ExecuteLineStroked(0, ACTION_COMMANDS[m_JoystickActionSecondary]); + m_JoystickActionSecondary = NUM_ACTIONS; + } + }; + ActionButton.m_IsVisibleFunction = IngameVisibleFunction; + m_vTouchButtons.emplace_back(std::move(ActionButton)); + + // TODO: The joystick button is only rendered round but currently it also accepts touches in the whole rectangle covering the circle. + // TODO: Should use DrawCircle function to render circle and remove m_BackgroundRounding again so it looks smoother. + const float JoystickButtonHeight = 48.0f; + const float JoystickButtonWidth = std::ceil(JoystickButtonHeight / (ScreenSize.x / ScreenSize.y)); + CTouchButton JoystickButton(this); + JoystickButton.m_UnitRect = ScaleRect(CUIRect{120.0f - JoystickButtonWidth - 2.0f, 120.0f - JoystickButtonHeight - 2.0f, JoystickButtonWidth, JoystickButtonHeight}, ButtonGridSize); + JoystickButton.m_ScreenRect = ScaleRect(JoystickButton.m_UnitRect, ScreenSize); + JoystickButton.m_LabelFunction = [&]() { return ACTION_NAMES[m_ActionSelected]; }; + JoystickButton.m_ActivateFunction = [&](CTouchButton &TouchButton) { + m_JoystickActionPrimary = m_ActionSelected; + GameClient()->m_Controls.m_aMousePos[g_Config.m_ClDummy] = (TouchButton.m_ActivePosition - vec2(0.5f, 0.5f)) * GameClient()->m_Controls.GetMaxMouseDistance(); + Console()->ExecuteLineStroked(1, ACTION_COMMANDS[m_JoystickActionPrimary]); + }; + JoystickButton.m_DeactivateFunction = [&](CTouchButton &TouchButton) { + Console()->ExecuteLineStroked(0, ACTION_COMMANDS[m_JoystickActionPrimary]); + m_JoystickActionPrimary = NUM_ACTIONS; + }; + JoystickButton.m_MovementFunction = [&](CTouchButton &TouchButton) { + GameClient()->m_Controls.m_aMousePos[g_Config.m_ClDummy] = (TouchButton.m_ActivePosition - vec2(0.5f, 0.5f)) * GameClient()->m_Controls.GetMaxMouseDistance(); + }; + JoystickButton.m_IsVisibleFunction = [&]() { return g_Config.m_ClTouchControlsJoystick && !m_pClient->m_Snap.m_SpecInfo.m_Active; }; + JoystickButton.m_BackgroundRounding = minimum(JoystickButton.m_ScreenRect.w, JoystickButton.m_ScreenRect.h) / 2.0f; + m_vTouchButtons.emplace_back(std::move(JoystickButton)); +} + +void CTouchControls::UpdateButtons(const std::vector &vTouchFingerStates) +{ + const bool DirectTouchEnabled = !g_Config.m_ClTouchControlsJoystick || m_pClient->m_Snap.m_SpecInfo.m_Active; + + std::vector vRemainingTouchFingerStates = vTouchFingerStates; + + // Remove touch fingers which have been released from the set of used touch fingers. + for(auto It = m_vUsedTouchFingers.begin(); It != m_vUsedTouchFingers.end();) + { + const auto ActiveFinger = std::find_if(vRemainingTouchFingerStates.begin(), vRemainingTouchFingerStates.end(), [&](const IInput::CTouchFingerState &TouchFingerState) { + return TouchFingerState.m_Finger == *It; + }); + if(ActiveFinger == vRemainingTouchFingerStates.end()) + { + It = m_vUsedTouchFingers.erase(It); + } + else + { + ++It; + } + } + + // Remove remaining finger states for fingers which are responsible for active actions + // and release action when the finger responsible for it is not pressed down anymore. + bool GotDirectFingerState = false; // Whether DirectFingerState is valid + IInput::CTouchFingerState DirectFingerState{}; // The finger that will be used to update the mouse position + for(int Action = ACTION_FIRE; Action < NUM_ACTIONS; ++Action) + { + if(!m_aActionStates[Action].m_Active) + continue; + + const auto ActiveFinger = std::find_if(vRemainingTouchFingerStates.begin(), vRemainingTouchFingerStates.end(), [&](const IInput::CTouchFingerState &TouchFingerState) { + return TouchFingerState.m_Finger == m_aActionStates[Action].m_Finger; + }); + if(ActiveFinger == vRemainingTouchFingerStates.end() || !DirectTouchEnabled) + { + m_aActionStates[Action].m_Active = false; + Console()->ExecuteLineStroked(0, ACTION_COMMANDS[Action]); + } + else + { + if(Action == m_ActionLastActivated) + { + GotDirectFingerState = true; + DirectFingerState = *ActiveFinger; + } + vRemainingTouchFingerStates.erase(ActiveFinger); + } + } + + // Update touch button states after the active action fingers were removed from the vector + // so that current cursor movement can cross over touch buttons without activating them. + for(CTouchButton &TouchButton : m_vTouchButtons) + { + bool Active = false; + if(TouchButton.m_IsVisibleFunction()) + { + while(!vRemainingTouchFingerStates.empty()) + { + const auto FingerInsideButton = std::find_if(vRemainingTouchFingerStates.begin(), vRemainingTouchFingerStates.end(), [&](const IInput::CTouchFingerState &TouchFingerState) { + return TouchButton.m_UnitRect.Inside(TouchFingerState.m_Position); + }); + if(FingerInsideButton == vRemainingTouchFingerStates.end()) + { + break; + } + TouchButton.SetActive(*FingerInsideButton); + if(std::find(m_vUsedTouchFingers.begin(), m_vUsedTouchFingers.end(), FingerInsideButton->m_Finger) == m_vUsedTouchFingers.end()) + { + m_vUsedTouchFingers.push_back(FingerInsideButton->m_Finger); + } + vRemainingTouchFingerStates.erase(FingerInsideButton); + Active = true; + } + } + if(!Active) + { + TouchButton.SetInactive(); + } + } + + // Fingers which are still pressed after having been used for a touch button will + // not activate the actions, to prevent accidental usage when leaving the buttons. + vRemainingTouchFingerStates.erase( + std::remove_if(vRemainingTouchFingerStates.begin(), vRemainingTouchFingerStates.end(), [&](const IInput::CTouchFingerState &TouchFingerState) { + return std::find(m_vUsedTouchFingers.begin(), m_vUsedTouchFingers.end(), TouchFingerState.m_Finger) != m_vUsedTouchFingers.end(); + }), + vRemainingTouchFingerStates.end()); + + // Activate action if there is an unhandled pressed down finger. + int ActivateAction = NUM_ACTIONS; + if(DirectTouchEnabled && !vRemainingTouchFingerStates.empty() && !m_aActionStates[m_ActionSelected].m_Active) + { + GotDirectFingerState = true; + DirectFingerState = vRemainingTouchFingerStates[0]; + vRemainingTouchFingerStates.erase(vRemainingTouchFingerStates.begin()); + m_aActionStates[m_ActionSelected].m_Active = true; + m_aActionStates[m_ActionSelected].m_Finger = DirectFingerState.m_Finger; + m_ActionLastActivated = m_ActionSelected; + ActivateAction = m_ActionSelected; + } + + // Update mouse position based on the finger responsible for the last active action. + if(GotDirectFingerState) + { + vec2 WorldScreenSize; + RenderTools()->CalcScreenParams(Graphics()->ScreenAspect(), m_pClient->m_Snap.m_SpecInfo.m_Active ? m_pClient->m_Camera.m_Zoom : 1.0f, &WorldScreenSize.x, &WorldScreenSize.y); + if(m_pClient->m_Snap.m_SpecInfo.m_Active) + { + GameClient()->m_Controls.m_aMousePos[g_Config.m_ClDummy] += -DirectFingerState.m_Delta * WorldScreenSize; + GameClient()->m_Controls.m_aMousePos[g_Config.m_ClDummy].x = clamp(GameClient()->m_Controls.m_aMousePos[g_Config.m_ClDummy].x, -201.0f * 32, (Collision()->GetWidth() + 201.0f) * 32.0f); + GameClient()->m_Controls.m_aMousePos[g_Config.m_ClDummy].y = clamp(GameClient()->m_Controls.m_aMousePos[g_Config.m_ClDummy].y, -201.0f * 32, (Collision()->GetHeight() + 201.0f) * 32.0f); + } + else + { + GameClient()->m_Controls.m_aMousePos[g_Config.m_ClDummy] = (DirectFingerState.m_Position - vec2(0.5f, 0.5f)) * WorldScreenSize; + } + } + + // Activate action after the mouse position is set. + if(ActivateAction != NUM_ACTIONS) + { + Console()->ExecuteLineStroked(1, ACTION_COMMANDS[m_ActionSelected]); + } +} + +void CTouchControls::RenderButtons() +{ + for(CTouchButton &TouchButton : m_vTouchButtons) + { + if(!TouchButton.m_IsVisibleFunction()) + continue; + + TouchButton.Render(); + } +} + +void CTouchControls::OnReset() +{ + for(CTouchButton &TouchButton : m_vTouchButtons) + { + TouchButton.m_Active = false; + } + for(CActionState &ActionState : m_aActionStates) + { + ActionState.m_Active = false; + } + m_vUsedTouchFingers.clear(); +} + +void CTouchControls::OnWindowResize() +{ + OnReset(); + m_vTouchButtons.clear(); // Recreate buttons to update positions and sizes +} + +bool CTouchControls::OnTouchState(const std::vector &vTouchFingerStates) +{ + if(!g_Config.m_ClTouchControls) + return false; + if(Client()->State() != IClient::STATE_ONLINE && Client()->State() != IClient::STATE_DEMOPLAYBACK) + return false; + if(GameClient()->m_Chat.IsActive() || !GameClient()->m_GameConsole.IsClosed() || GameClient()->m_Menus.IsActive()) + { + OnReset(); + return false; + } + UpdateButtons(vTouchFingerStates); + return true; +} + +void CTouchControls::OnRender() +{ + if(!g_Config.m_ClTouchControls) + return; + if(Client()->State() != IClient::STATE_ONLINE && Client()->State() != IClient::STATE_DEMOPLAYBACK) + return; + if(GameClient()->m_Chat.IsActive() || !GameClient()->m_GameConsole.IsClosed() || GameClient()->m_Menus.IsActive()) + return; + + const float ScreenHeight = 400.0f * 3.0f; + const float ScreenWidth = ScreenHeight * Graphics()->ScreenAspect(); + const vec2 ScreenSize = vec2(ScreenWidth, ScreenHeight); + Graphics()->MapScreen(0.0f, 0.0f, ScreenWidth, ScreenHeight); + + if(m_vTouchButtons.empty()) + { + InitButtons(ScreenSize); + } + RenderButtons(); +} diff --git a/src/game/client/components/touch_controls.h b/src/game/client/components/touch_controls.h new file mode 100644 index 00000000000..319dee5277d --- /dev/null +++ b/src/game/client/components/touch_controls.h @@ -0,0 +1,89 @@ +#ifndef GAME_CLIENT_COMPONENTS_TOUCH_CONTROLS_H +#define GAME_CLIENT_COMPONENTS_TOUCH_CONTROLS_H + +#include + +#include + +#include +#include + +#include +#include +#include + +class CTouchControls : public CComponent +{ + // TODO: Encapsulate behavior and rendering in subclasses with virtual functions instead of using std::functions? + class CTouchButton + { + CTouchControls *m_pTouchControls; + + public: + CTouchButton(CTouchControls *pTouchControls); + + CUIRect m_UnitRect; + CUIRect m_ScreenRect; + + std::function m_LabelFunction; + std::function m_ActivateFunction; + std::function m_DeactivateFunction; + std::function m_MovementFunction; // can be nullptr + std::function m_IsVisibleFunction; + int m_BackgroundCorners; + float m_BackgroundRounding; + + bool m_Active; // variables below must only be used when active + IInput::CTouchFinger m_Finger; + vec2 m_ActivePosition; + vec2 m_AccumulatedDelta; + float m_ActivationStartTime; + + void SetActive(const IInput::CTouchFingerState &FingerState); + void SetInactive(); + void Render(); + }; + + std::vector m_vTouchButtons; + + /** + * Touch fingers which will be ignored for activating the primary action. + * These are fingers which were used for touch buttons previously. They also + * include fingers which are still pressed but not activating any button. + */ + std::vector m_vUsedTouchFingers; + + bool m_ExtraButtonsActive = false; + + enum + { + ACTION_FIRE = 0, + ACTION_HOOK, + NUM_ACTIONS + }; + + class CActionState + { + public: + bool m_Active = false; + IInput::CTouchFinger m_Finger; + }; + + int m_ActionSelected = ACTION_FIRE; + int m_ActionLastActivated = ACTION_FIRE; + CActionState m_aActionStates[NUM_ACTIONS]; + int m_JoystickActionPrimary = NUM_ACTIONS; + int m_JoystickActionSecondary = NUM_ACTIONS; + + void InitButtons(vec2 ScreenSize); + void UpdateButtons(const std::vector &vTouchFingerStates); + void RenderButtons(); + +public: + int Sizeof() const override { return sizeof(*this); } + void OnReset() override; + void OnWindowResize() override; + bool OnTouchState(const std::vector &vTouchFingerStates) override; + void OnRender() override; +}; +#endif diff --git a/src/game/client/components/voting.cpp b/src/game/client/components/voting.cpp index 0c2fbb57ed8..e7094647f86 100644 --- a/src/game/client/components/voting.cpp +++ b/src/game/client/components/voting.cpp @@ -146,6 +146,12 @@ int CVoting::SecondsLeft() const return (m_Closetime - time()) / time_freq(); } +bool CVoting::IsActive() const +{ + return IsVoting() && Client()->State() != IClient::STATE_DEMOPLAYBACK && + (g_Config.m_ClShowVotesAfterVoting || m_pClient->m_Scoreboard.Active() || !TakenChoice()); +} + CVoting::CVoting() { ClearOptions(); @@ -328,8 +334,9 @@ void CVoting::OnMessage(int MsgType, void *pRawMsg) void CVoting::Render() { - if((!g_Config.m_ClShowVotesAfterVoting && !m_pClient->m_Scoreboard.Active() && TakenChoice()) || !IsVoting() || Client()->State() == IClient::STATE_DEMOPLAYBACK) + if(!IsActive()) return; + const int Seconds = SecondsLeft(); if(Seconds < 0) { diff --git a/src/game/client/components/voting.h b/src/game/client/components/voting.h index 2d3d36d15bd..31199cf2bd0 100644 --- a/src/game/client/components/voting.h +++ b/src/game/client/components/voting.h @@ -63,6 +63,7 @@ class CVoting : public CComponent const char *VoteDescription() const { return m_aDescription; } const char *VoteReason() const { return m_aReason; } bool IsReceivingOptions() const { return m_ReceivingOptions; } + bool IsActive() const; }; #endif diff --git a/src/game/client/gameclient.cpp b/src/game/client/gameclient.cpp index af6ea217d1e..8503eb9dbb3 100644 --- a/src/game/client/gameclient.cpp +++ b/src/game/client/gameclient.cpp @@ -137,6 +137,7 @@ void CGameClient::OnConsoleInit() &m_Chat, &m_Broadcast, &m_DebugHud, + &m_TouchControls, &m_Scoreboard, &m_Statboard, &m_Motd, @@ -156,6 +157,7 @@ void CGameClient::OnConsoleInit() &m_Spectator, &m_Emoticon, &m_Controls, + &m_TouchControls, &m_Binds}); // add basic console commands @@ -381,6 +383,17 @@ void CGameClient::OnUpdate() } } + // handle touch events + std::vector vTouchFingerStates = Input()->TouchFingerStates(); + for(auto &pComponent : m_vpInput) + { + if(pComponent->OnTouchState(vTouchFingerStates)) + { + Input()->ClearTouchDeltas(); + break; + } + } + // handle key presses Input()->ConsumeEvents([&](const IInput::CEvent &Event) { for(auto &pComponent : m_vpInput) diff --git a/src/game/client/gameclient.h b/src/game/client/gameclient.h index 0d4eae079c0..aebb7518272 100644 --- a/src/game/client/gameclient.h +++ b/src/game/client/gameclient.h @@ -52,6 +52,7 @@ #include "components/spectator.h" #include "components/statboard.h" #include "components/tooltips.h" +#include "components/touch_controls.h" #include "components/voting.h" class CGameInfo @@ -132,6 +133,7 @@ class CGameClient : public IGameClient CSounds m_Sounds; CEmoticon m_Emoticon; CDamageInd m_DamageInd; + CTouchControls m_TouchControls; CVoting m_Voting; CSpectator m_Spectator; diff --git a/src/game/client/ui_rect.h b/src/game/client/ui_rect.h index 54d7b178955..4e55d4607d9 100644 --- a/src/game/client/ui_rect.h +++ b/src/game/client/ui_rect.h @@ -136,7 +136,9 @@ class CUIRect void Draw(ColorRGBA Color, int Corners, float Rounding) const; void Draw4(ColorRGBA ColorTopLeft, ColorRGBA ColorTopRight, ColorRGBA ColorBottomLeft, ColorRGBA ColorBottomRight, int Corners, float Rounding) const; - vec2 Center() const { return vec2(x + w / 2.0f, y + h / 2.0f); } + vec2 TopLeft() const { return vec2(x, y); } + vec2 Size() const { return vec2(w, h); } + vec2 Center() const { return TopLeft() + Size() / 2.0f; } }; #endif