From 88b1b287e12f55b944ac9f28fb137638429edf39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20M=C3=BCller?= Date: Sat, 13 Jul 2024 22:25:34 +0200 Subject: [PATCH] Add ingame touch controls MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add new client component `CTouchControls` for playing the game with touch inputs. The touch controls can be enabled/disabled with the config variable `cl_touch_controls`, which defaults to 1 on Android and 0 on other platforms. The touch controls consist of various on-screen touch buttons. The buttons are only shown when they are usable depending on the context. Movement buttons for Left, Right and Jump actions are arranged in a `⊥`-pattern similar to WASD controls. For the fire and hook action, two modes are implemented: 1. Direct touch input: the mouse is moved exactly where the player touches on the screen. 2. Virtual joystick: a button is used to emulate a joystick, which moves the mouse relative to the player. In either mode, a button is used to switch between the active actions (fire and hook). While the virtual joystick is being held down, this button uses the other action directly instead of switching it. The direct touch input is disabled while the virtual joystick is enabled, to prevent accidental direct touch input in this mode. The config variable `cl_touch_controls_joystick` toggles which mode is active. When spectating, direct touch input is always used to allow panning the map directly like in an image/map viewer. Two separate buttons are shown to switch to the previous and next weapons. A hamburger menu button `☰` is used to toggle the visibility of lesser used touch buttons. Long pressing this button will open the regular menu. This includes buttons for showing the scoreboard, showing the emoticon HUD, showing the spectator HUD, opening team and team chat, voting yes/no, and zooming. Once the dummy is connected, a button for swapping between main and dummy is shown. The emoticon and spectator HUDs are activated with the respective touch buttons and can be deactivated by touching outside of them or by using the back-button, as toggling them while the ingame touch button is pressed down is currently not feasible and also inconvenient at least for using the spectator HUD. In addition to the separate on-screen touch controls, a second row is added to the main page of the ingame menu when `cl_touch_controls` is enabled for less frequently used functions which are otherwise not usable without a keyboard: - Checkbox for toggling the virtual joystick. - Checkbox for toggling entities. - Buttons to open the local and remote consoles. - Button to close the menu (more convenient than using the back button if it's not always shown). Currently missing: - Various decisions and code cleanup TODOs in `CTouchControls`. --- CMakeLists.txt | 2 + src/engine/client/input.cpp | 13 +- src/engine/client/input.h | 1 + src/engine/input.h | 6 + src/engine/shared/config_variables.h | 6 + src/game/client/component.h | 8 + src/game/client/components/controls.h | 2 +- src/game/client/components/emoticon.cpp | 80 ++- src/game/client/components/emoticon.h | 8 + src/game/client/components/menus.h | 3 +- src/game/client/components/menus_ingame.cpp | 75 ++- src/game/client/components/spectator.cpp | 44 +- src/game/client/components/spectator.h | 6 + src/game/client/components/touch_controls.cpp | 635 ++++++++++++++++++ src/game/client/components/touch_controls.h | 91 +++ src/game/client/components/voting.cpp | 9 +- src/game/client/components/voting.h | 1 + src/game/client/gameclient.cpp | 16 +- src/game/client/gameclient.h | 2 + src/game/client/ui_rect.h | 4 +- 20 files changed, 955 insertions(+), 57 deletions(-) create mode 100644 src/game/client/components/touch_controls.cpp create mode 100644 src/game/client/components/touch_controls.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 7c85dd345e3..666feb4982f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2395,6 +2395,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 5711a5119af..19812c48ca1 100644 --- a/src/engine/client/input.cpp +++ b/src/engine/client/input.cpp @@ -303,6 +303,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); @@ -349,10 +357,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 908e133e141..50d282a45af 100644 --- a/src/engine/client/input.h +++ b/src/engine/client/input.h @@ -150,6 +150,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 4bb6e67a737..a7a8f746d90 100644 --- a/src/engine/input.h +++ b/src/engine/input.h @@ -136,6 +136,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 417807e84f3..ad5859fa6bd 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/emoticon.cpp b/src/game/client/components/emoticon.cpp index 9d0d2b91061..2a27ba21453 100644 --- a/src/game/client/components/emoticon.cpp +++ b/src/game/client/components/emoticon.cpp @@ -41,6 +41,7 @@ void CEmoticon::OnReset() m_Active = false; m_SelectedEmote = -1; m_SelectedEyeEmote = -1; + m_TouchPressedOutside = false; } void CEmoticon::OnRelease() @@ -58,6 +59,16 @@ bool CEmoticon::OnCursorMove(float x, float y, IInput::ECursorType CursorType) return true; } +bool CEmoticon::OnInput(const IInput::CEvent &Event) +{ + if(IsActive() && Event.m_Flags & IInput::FLAG_PRESS && Event.m_Key == KEY_ESCAPE) + { + OnRelease(); + return true; + } + return false; +} + void CEmoticon::OnRender() { if(Client()->State() != IClient::STATE_ONLINE && Client()->State() != IClient::STATE_DEMOPLAYBACK) @@ -65,6 +76,13 @@ void CEmoticon::OnRender() if(!m_Active) { + if(m_TouchPressedOutside) + { + m_SelectedEmote = -1; + m_SelectedEyeEmote = -1; + m_TouchPressedOutside = false; + } + if(m_WasActive && m_SelectedEmote != -1) Emote(m_SelectedEmote); if(m_WasActive && m_SelectedEyeEmote != -1) @@ -82,6 +100,31 @@ void CEmoticon::OnRender() m_WasActive = true; + CUIRect Screen = *Ui()->Screen(); + const vec2 ScreenSize = vec2(Screen.w, Screen.h); + const vec2 ScreenCenter = ScreenSize / 2.0f; + + const bool WasTouchPressed = m_TouchState.m_AnyPressed; + Ui()->UpdateTouchState(m_TouchState); + if(m_TouchState.m_AnyPressed) + { + const vec2 TouchPos = (m_TouchState.m_PrimaryPosition - vec2(0.5f, 0.5f)) * ScreenSize; + const float TouchCenterDistance = length(TouchPos); + if(TouchCenterDistance <= 170.0f) + { + m_SelectorMouse = TouchPos; + } + else if(TouchCenterDistance > 190.0f) + { + m_TouchPressedOutside = true; + } + } + else if(WasTouchPressed) + { + m_Active = false; + return; + } + if(length(m_SelectorMouse) > 170.0f) m_SelectorMouse = normalize(m_SelectorMouse) * 170.0f; @@ -96,8 +139,6 @@ void CEmoticon::OnRender() else if(length(m_SelectorMouse) > 40.0f) m_SelectedEyeEmote = (int)(SelectedAngle / (2 * pi) * NUM_EMOTES); - CUIRect Screen = *Ui()->Screen(); - Ui()->MapScreen(); Graphics()->BlendNormal(); @@ -105,26 +146,22 @@ void CEmoticon::OnRender() Graphics()->TextureClear(); Graphics()->QuadsBegin(); Graphics()->SetColor(0, 0, 0, 0.3f); - Graphics()->DrawCircle(Screen.w / 2, Screen.h / 2, 190.0f, 64); + Graphics()->DrawCircle(ScreenCenter.x, ScreenCenter.y, 190.0f, 64); Graphics()->QuadsEnd(); Graphics()->WrapClamp(); - for(int i = 0; i < NUM_EMOTICONS; i++) + for(int Emote = 0; Emote < NUM_EMOTICONS; Emote++) { - float Angle = 2 * pi * i / NUM_EMOTICONS; + float Angle = 2 * pi * Emote / NUM_EMOTICONS; if(Angle > pi) Angle -= 2 * pi; - bool Selected = m_SelectedEmote == i; - - float Size = Selected ? 80.0f : 50.0f; - - Graphics()->TextureSet(GameClient()->m_EmoticonsSkin.m_aSpriteEmoticons[i]); + Graphics()->TextureSet(GameClient()->m_EmoticonsSkin.m_aSpriteEmoticons[Emote]); Graphics()->QuadsSetSubset(0, 0, 1, 1); - Graphics()->QuadsBegin(); const vec2 Nudge = direction(Angle) * 150.0f; - IGraphics::CQuadItem QuadItem(Screen.w / 2 + Nudge.x, Screen.h / 2 + Nudge.y, Size, Size); + const float Size = m_SelectedEmote == Emote ? 80.0f : 50.0f; + IGraphics::CQuadItem QuadItem(ScreenCenter.x + Nudge.x, ScreenCenter.y + Nudge.y, Size, Size); Graphics()->QuadsDraw(&QuadItem, 1); Graphics()->QuadsEnd(); } @@ -135,34 +172,29 @@ void CEmoticon::OnRender() Graphics()->TextureClear(); Graphics()->QuadsBegin(); Graphics()->SetColor(1.0, 1.0, 1.0, 0.3f); - Graphics()->DrawCircle(Screen.w / 2, Screen.h / 2, 100.0f, 64); + Graphics()->DrawCircle(ScreenCenter.x, ScreenCenter.y, 100.0f, 64); Graphics()->QuadsEnd(); CTeeRenderInfo TeeInfo = m_pClient->m_aClients[m_pClient->m_aLocalIds[g_Config.m_ClDummy]].m_RenderInfo; - for(int i = 0; i < NUM_EMOTES; i++) + for(int Emote = 0; Emote < NUM_EMOTES; Emote++) { - float Angle = 2 * pi * i / NUM_EMOTES; - if(Angle > pi) - Angle -= 2 * pi; - - const bool Selected = m_SelectedEyeEmote == i; - + const float Angle = std::fmod(2 * pi * Emote / NUM_EMOTES, 2 * pi); const vec2 Nudge = direction(Angle) * 70.0f; - TeeInfo.m_Size = Selected ? 64.0f : 48.0f; - RenderTools()->RenderTee(CAnimState::GetIdle(), &TeeInfo, i, vec2(-1, 0), vec2(Screen.w / 2 + Nudge.x, Screen.h / 2 + Nudge.y)); + TeeInfo.m_Size = m_SelectedEyeEmote == Emote ? 64.0f : 48.0f; + RenderTools()->RenderTee(CAnimState::GetIdle(), &TeeInfo, Emote, vec2(-1, 0), ScreenCenter + Nudge); } Graphics()->TextureClear(); Graphics()->QuadsBegin(); Graphics()->SetColor(0, 0, 0, 0.3f); - Graphics()->DrawCircle(Screen.w / 2, Screen.h / 2, 30.0f, 64); + Graphics()->DrawCircle(ScreenCenter.x, ScreenCenter.y, 30.0f, 64); Graphics()->QuadsEnd(); } else m_SelectedEyeEmote = -1; - RenderTools()->RenderCursor(m_SelectorMouse + vec2(Screen.w, Screen.h) / 2, 24.0f); + RenderTools()->RenderCursor(ScreenCenter + m_SelectorMouse, 24.0f); } void CEmoticon::Emote(int Emoticon) diff --git a/src/game/client/components/emoticon.h b/src/game/client/components/emoticon.h index c7d068cb1bb..890f1c80efe 100644 --- a/src/game/client/components/emoticon.h +++ b/src/game/client/components/emoticon.h @@ -4,7 +4,9 @@ #define GAME_CLIENT_COMPONENTS_EMOTICON_H #include #include + #include +#include class CEmoticon : public CComponent { @@ -15,6 +17,9 @@ class CEmoticon : public CComponent int m_SelectedEmote; int m_SelectedEyeEmote; + CUi::CTouchState m_TouchState; + bool m_TouchPressedOutside; + static void ConKeyEmoticon(IConsole::IResult *pResult, void *pUserData); static void ConEmote(IConsole::IResult *pResult, void *pUserData); @@ -27,9 +32,12 @@ class CEmoticon : public CComponent virtual void OnRender() override; virtual void OnRelease() override; virtual bool OnCursorMove(float x, float y, IInput::ECursorType CursorType) override; + virtual bool OnInput(const IInput::CEvent &Event) override; void Emote(int Emoticon); void EyeEmote(int EyeEmote); + + bool IsActive() const { return m_Active; } }; #endif diff --git a/src/game/client/components/menus.h b/src/game/client/components/menus.h index 1064aca7c47..35fd9a729b5 100644 --- a/src/game/client/components/menus.h +++ b/src/game/client/components/menus.h @@ -607,7 +607,6 @@ class CMenus : public CComponent void RenderSettingsCustom(CUIRect MainView); void SetNeedSendInfo(); - void SetActive(bool Active); void UpdateColors(); IGraphics::CTextureHandle m_TextureBlob; @@ -627,6 +626,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 53f0a3a7454..cbe0b793dd1 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); static CButtonContainer s_DummyButton; @@ -107,9 +103,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)) @@ -134,7 +129,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(!Client()->DummyConnecting() && DoButton_Menu(&s_SpectateButton, Localize("Spectate"), 0, &Button)) { @@ -150,7 +144,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(!Client()->DummyConnecting() && DoButton_Menu(&s_JoinRedButton, Localize("Join red"), 0, &Button)) @@ -162,7 +156,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(!Client()->DummyConnecting() && DoButton_Menu(&s_JoinBlueButton, Localize("Join blue"), 0, &Button)) @@ -176,7 +170,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(!Client()->DummyConnecting() && DoButton_Menu(&s_SpectateButton, Localize("Join game"), 0, &Button)) { @@ -188,7 +182,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; @@ -204,17 +198,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/spectator.cpp b/src/game/client/components/spectator.cpp index 11b4fd38322..e1e79e5f51b 100644 --- a/src/game/client/components/spectator.cpp +++ b/src/game/client/components/spectator.cpp @@ -186,6 +186,16 @@ bool CSpectator::OnCursorMove(float x, float y, IInput::ECursorType CursorType) return true; } +bool CSpectator::OnInput(const IInput::CEvent &Event) +{ + if(IsActive() && Event.m_Flags & IInput::FLAG_PRESS && Event.m_Key == KEY_ESCAPE) + { + OnRelease(); + return true; + } + return false; +} + void CSpectator::OnRelease() { OnReset(); @@ -258,8 +268,6 @@ void CSpectator::OnRender() float BoxMove = -10.0f; float BoxOffset = 0.0f; - const bool MousePressed = Input()->KeyPress(KEY_MOUSE_1); - for(const auto &pInfo : m_pClient->m_Snap.m_apInfoByDDTeamName) { if(!pInfo || pInfo->m_Team == TEAM_SPECTATORS) @@ -293,14 +301,42 @@ void CSpectator::OnRender() ObjWidth = 600.0f; } + const vec2 ScreenSize = vec2(Width, Height); + const vec2 ScreenCenter = ScreenSize / 2.0f; + CUIRect SpectatorRect = {Width / 2.0f - ObjWidth, Height / 2.0f - 300.0f, ObjWidth * 2.0f, 600.0f}; + CUIRect SpectatorMouseRect; + SpectatorRect.Margin(20.0f, &SpectatorMouseRect); + + const bool WasTouchPressed = m_TouchState.m_AnyPressed; + Ui()->UpdateTouchState(m_TouchState); + if(m_TouchState.m_AnyPressed) + { + const vec2 TouchPos = (m_TouchState.m_PrimaryPosition - vec2(0.5f, 0.5f)) * ScreenSize; + if(SpectatorMouseRect.Inside(ScreenCenter + TouchPos)) + { + m_SelectorMouse = TouchPos; + } + } + else if(WasTouchPressed) + { + const vec2 TouchPos = (m_TouchState.m_PrimaryPosition - vec2(0.5f, 0.5f)) * ScreenSize; + if(!SpectatorRect.Inside(ScreenCenter + TouchPos)) + { + OnRelease(); + return; + } + } + Graphics()->MapScreen(0, 0, Width, Height); - Graphics()->DrawRect(Width / 2.0f - ObjWidth, Height / 2.0f - 300.0f, ObjWidth * 2, 600.0f, ColorRGBA(0.0f, 0.0f, 0.0f, 0.3f), IGraphics::CORNER_ALL, 20.0f); + SpectatorRect.Draw(ColorRGBA(0.0f, 0.0f, 0.0f, 0.3f), IGraphics::CORNER_ALL, 20.0f); // clamp mouse position to selector area m_SelectorMouse.x = clamp(m_SelectorMouse.x, -(ObjWidth - 20.0f), ObjWidth - 20.0f); m_SelectorMouse.y = clamp(m_SelectorMouse.y, -280.0f, 280.0f); + const bool MousePressed = Input()->KeyPress(KEY_MOUSE_1) || m_TouchState.m_PrimaryPressed; + // draw selections if((Client()->State() == IClient::STATE_DEMOPLAYBACK && m_pClient->m_DemoSpecId == SPEC_FREEVIEW) || (Client()->State() != IClient::STATE_DEMOPLAYBACK && m_pClient->m_Snap.m_SpecInfo.m_SpectatorId == SPEC_FREEVIEW)) @@ -534,7 +570,7 @@ void CSpectator::OnRender() } TextRender()->TextColor(1.0f, 1.0f, 1.0f, 1.0f); - RenderTools()->RenderCursor(m_SelectorMouse + vec2(Width, Height) / 2, 48.0f); + RenderTools()->RenderCursor(ScreenCenter + m_SelectorMouse, 48.0f); } void CSpectator::OnReset() diff --git a/src/game/client/components/spectator.h b/src/game/client/components/spectator.h index e7fdc2449e6..25dddfb60e4 100644 --- a/src/game/client/components/spectator.h +++ b/src/game/client/components/spectator.h @@ -6,6 +6,7 @@ #include #include +#include class CSpectator : public CComponent { @@ -21,6 +22,8 @@ class CSpectator : public CComponent int m_SelectedSpectatorId; vec2 m_SelectorMouse; + CUi::CTouchState m_TouchState; + float m_MultiViewActivateDelay; bool CanChangeSpectator(); @@ -39,12 +42,15 @@ class CSpectator : public CComponent virtual void OnConsoleInit() override; virtual bool OnCursorMove(float x, float y, IInput::ECursorType CursorType) override; + virtual bool OnInput(const IInput::CEvent &Event) override; virtual void OnRender() override; virtual void OnRelease() override; virtual void OnReset() override; void Spectate(int SpectatorId); void SpectateClosest(); + + bool IsActive() const { return m_Active; } }; #endif diff --git a/src/game/client/components/touch_controls.cpp b/src/game/client/components/touch_controls.cpp new file mode 100644 index 00000000000..448829a67e6 --- /dev/null +++ b/src/game/client/components/touch_controls.cpp @@ -0,0 +1,635 @@ +#include "touch_controls.h" + +#include +#include + +#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_ButtonShape(CTouchControls::EButtonShape::RECT), + m_BackgroundCorners(IGraphics::CORNER_ALL), + m_Active(false) +{ +} + +vec2 CTouchControls::CTouchButton::ClampTouchPosition(vec2 TouchPosition) const +{ + switch(m_ButtonShape) + { + case EButtonShape::RECT: + { + TouchPosition.x = clamp(TouchPosition.x, m_ScreenRect.x, m_ScreenRect.x + m_ScreenRect.w); + TouchPosition.y = clamp(TouchPosition.y, m_ScreenRect.y, m_ScreenRect.y + m_ScreenRect.h); + break; + } + case EButtonShape::CIRCLE: + { + const vec2 Center = m_ScreenRect.Center(); + const float Radius = minimum(m_ScreenRect.w, m_ScreenRect.h) / 2.0f; + if(distance(TouchPosition, Center) > Radius) + { + TouchPosition = normalize(TouchPosition - Center) * Radius + Center; + } + break; + } + default: + dbg_assert(false, "Unhandled shape"); + break; + } + return TouchPosition; +} + +bool CTouchControls::CTouchButton::IsInside(vec2 TouchPosition) const +{ + switch(m_ButtonShape) + { + case EButtonShape::RECT: + return m_ScreenRect.Inside(TouchPosition); + case EButtonShape::CIRCLE: + return distance(TouchPosition, m_ScreenRect.Center()) <= minimum(m_ScreenRect.w, m_ScreenRect.h) / 2.0f; + default: + dbg_assert(false, "Unhandled shape"); + return false; + } +} + +void CTouchControls::CTouchButton::SetActive(const IInput::CTouchFingerState &FingerState) +{ + const vec2 ScreenSize = m_pTouchControls->CalculateScreenSize(); + const vec2 Position = (ClampTouchPosition(FingerState.m_Position * ScreenSize) - m_ScreenRect.TopLeft()) / m_ScreenRect.Size(); + const vec2 Delta = FingerState.m_Delta * ScreenSize / m_ScreenRect.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); + } + } +} + +void CTouchControls::CTouchButton::SetInactive() +{ + if(m_Active) + { + m_Active = false; + m_DeactivateFunction(*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); + + switch(m_ButtonShape) + { + case EButtonShape::RECT: + { + m_ScreenRect.Draw(ButtonColor, m_BackgroundCorners, 10.0f); + break; + } + case EButtonShape::CIRCLE: + { + const vec2 Center = m_ScreenRect.Center(); + const float Radius = minimum(m_ScreenRect.w, m_ScreenRect.h) / 2.0f; + m_pTouchControls->Graphics()->TextureClear(); + m_pTouchControls->Graphics()->QuadsBegin(); + m_pTouchControls->Graphics()->SetColor(ButtonColor); + m_pTouchControls->Graphics()->DrawCircle(Center.x, Center.y, Radius, round_truncate(Radius / 4.0f)); + m_pTouchControls->Graphics()->QuadsEnd(); + break; + } + default: + dbg_assert(false, "Unhandled shape"); + break; + } + + 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); +} + +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; +} + +// TODO: Adjustable button layout +void CTouchControls::InitButtons() +{ + const vec2 ScreenSize = CalculateScreenSize(); + 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(); + }; + const auto &&DummyVisibleFunction = [&]() { + return Client()->DummyAllowed() && Client()->DummyConnected(); + }; + + // 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)); + + 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 "← 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 "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) { Console()->ExecuteLineStroked(1, "zoom-"); }; + ZoomOutButton.m_DeactivateFunction = [&](CTouchButton &TouchButton) { Console()->ExecuteLineStroked(0, "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) { Console()->ExecuteLineStroked(1, "zoom"); }; + ZoomDefaultButton.m_DeactivateFunction = [&](CTouchButton &TouchButton) { Console()->ExecuteLineStroked(0, "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) { Console()->ExecuteLineStroked(1, "zoom+"); }; + ZoomInButton.m_DeactivateFunction = [&](CTouchButton &TouchButton) { Console()->ExecuteLineStroked(0, "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)); + + 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"; }; + EmoticonButton.m_ActivateFunction = [](CTouchButton &TouchButton) {}; + EmoticonButton.m_DeactivateFunction = [&](CTouchButton &TouchButton) { Console()->ExecuteLineStroked(1, "+emote"); }; + EmoticonButton.m_IsVisibleFunction = ExtraButtonIngameVisibleFunction; + m_vTouchButtons.emplace_back(std::move(EmoticonButton)); + + 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"; }; // Emoticon and spectate buttons have same position + SpectateButton.m_ActivateFunction = [](CTouchButton &TouchButton) {}; + SpectateButton.m_DeactivateFunction = [&](CTouchButton &TouchButton) { Console()->ExecuteLineStroked(1, "+spectate"); }; + 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_ActivateFunction = [&](CTouchButton &TouchButton) { Console()->ExecuteLineStroked(1, "+show_chat; chat all"); }; + AllChatButton.m_DeactivateFunction = [&](CTouchButton &TouchButton) { Console()->ExecuteLineStroked(0, "+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) { Console()->ExecuteLineStroked(1, "+show_chat; chat team"); }; + TeamChatButton.m_DeactivateFunction = [&](CTouchButton &TouchButton) { Console()->ExecuteLineStroked(0, "+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) { Console()->ExecuteLineStroked(1, "vote yes"); }; + VoteYesButton.m_DeactivateFunction = [&](CTouchButton &TouchButton) { Console()->ExecuteLineStroked(0, "vote yes"); }; + VoteYesButton.m_IsVisibleFunction = VoteVisibleFunction; + 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) { Console()->ExecuteLineStroked(1, "vote no"); }; + VoteNoButton.m_DeactivateFunction = [&](CTouchButton &TouchButton) { Console()->ExecuteLineStroked(0, "vote no"); }; + VoteNoButton.m_IsVisibleFunction = VoteVisibleFunction; + 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 = []() { return "Toggle dummy"; }; + ToggleDummyButton.m_ActivateFunction = [&](CTouchButton &TouchButton) { Console()->ExecuteLineStroked(1, "toggle cl_dummy 0 1"); }; + ToggleDummyButton.m_DeactivateFunction = [&](CTouchButton &TouchButton) { Console()->ExecuteLineStroked(0, "toggle cl_dummy 0 1"); }; + ToggleDummyButton.m_IsVisibleFunction = DummyVisibleFunction; + 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)); + + 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_ButtonShape = EButtonShape::CIRCLE; + 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; + const vec2 ScreenSize = CalculateScreenSize(); + + std::vector vRemainingTouchFingerStates = vTouchFingerStates; + + // 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. + + // Activate visible, inactive buttons with hovered finger. Deactivate previous button + // being activated by the same finger. + for(CTouchButton &TouchButton : m_vTouchButtons) + { + if(!TouchButton.m_IsVisibleFunction() || TouchButton.m_Active) + { + continue; + } + const auto FingerInsideButton = std::find_if(vRemainingTouchFingerStates.begin(), vRemainingTouchFingerStates.end(), [&](const IInput::CTouchFingerState &TouchFingerState) { + return TouchButton.IsInside(TouchFingerState.m_Position * ScreenSize); + }); + if(FingerInsideButton == vRemainingTouchFingerStates.end()) + { + continue; + } + auto OtherTouchButton = std::find_if(m_vTouchButtons.begin(), m_vTouchButtons.end(), [&](const CTouchButton &Button) { + return Button.m_Active && Button.m_Finger == FingerInsideButton->m_Finger; + }); + if(OtherTouchButton != m_vTouchButtons.end()) + { + OtherTouchButton->SetInactive(); + } + TouchButton.SetActive(*FingerInsideButton); + } + + // Deactivate touch buttons only when the respective finger is released, so touch buttons + // are kept active also if the finger is moved outside the button. + for(CTouchButton &TouchButton : m_vTouchButtons) + { + if(!TouchButton.m_IsVisibleFunction()) + { + TouchButton.SetInactive(); + continue; + } + if(!TouchButton.m_Active) + { + continue; + } + const auto ActiveFinger = std::find_if(vRemainingTouchFingerStates.begin(), vRemainingTouchFingerStates.end(), [&](const IInput::CTouchFingerState &TouchFingerState) { + return TouchFingerState.m_Finger == TouchButton.m_Finger; + }); + if(ActiveFinger == vRemainingTouchFingerStates.end()) + { + TouchButton.SetInactive(); + } + else + { + // Update the already active touch button with the current finger state + TouchButton.SetActive(*ActiveFinger); + } + } + + // Remove remaining fingers for active buttons after updating the buttons. + for(CTouchButton &TouchButton : m_vTouchButtons) + { + if(!TouchButton.m_Active) + { + continue; + } + const auto ActiveFinger = std::find_if(vRemainingTouchFingerStates.begin(), vRemainingTouchFingerStates.end(), [&](const IInput::CTouchFingerState &TouchFingerState) { + return TouchFingerState.m_Finger == TouchButton.m_Finger; + }); + if(ActiveFinger == vRemainingTouchFingerStates.end()) + { + continue; + } + vRemainingTouchFingerStates.erase(ActiveFinger); + } + + // 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(); + } +} + +vec2 CTouchControls::CalculateScreenSize() const +{ + const float ScreenHeight = 400.0f * 3.0f; + const float ScreenWidth = ScreenHeight * Graphics()->ScreenAspect(); + return vec2(ScreenWidth, ScreenHeight); +} + +void CTouchControls::OnReset() +{ + for(CTouchButton &TouchButton : m_vTouchButtons) + { + TouchButton.m_Active = false; + } + for(CActionState &ActionState : m_aActionStates) + { + ActionState.m_Active = false; + } +} + +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() || + GameClient()->m_Emoticon.IsActive() || + GameClient()->m_Spectator.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_Emoticon.IsActive() || GameClient()->m_Spectator.IsActive()) + return; + + const vec2 ScreenSize = CalculateScreenSize(); + Graphics()->MapScreen(0.0f, 0.0f, ScreenSize.x, ScreenSize.y); + + if(m_vTouchButtons.empty()) + { + InitButtons(); + } + 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..bb9ebf1909b --- /dev/null +++ b/src/game/client/components/touch_controls.h @@ -0,0 +1,91 @@ +#ifndef GAME_CLIENT_COMPONENTS_TOUCH_CONTROLS_H +#define GAME_CLIENT_COMPONENTS_TOUCH_CONTROLS_H + +#include + +#include + +#include +#include + +#include +#include + +class CTouchControls : public CComponent +{ + enum class EButtonShape + { + RECT, + CIRCLE + }; + + // 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; + + EButtonShape m_ButtonShape; + int m_BackgroundCorners; // only used with EButtonShape::RECT + + bool m_Active; // variables below must only be used when active + IInput::CTouchFinger m_Finger; + vec2 m_ActivePosition; + vec2 m_AccumulatedDelta; + float m_ActivationStartTime; + + vec2 ClampTouchPosition(vec2 TouchPosition) const; + bool IsInside(vec2 TouchPosition) const; + void SetActive(const IInput::CTouchFingerState &FingerState); + void SetInactive(); + void Render(); + }; + + std::vector m_vTouchButtons; + + 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(); + void UpdateButtons(const std::vector &vTouchFingerStates); + void RenderButtons(); + vec2 CalculateScreenSize() const; + +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 8a0282fdbb0..7f0c9b28384 100644 --- a/src/game/client/components/voting.cpp +++ b/src/game/client/components/voting.cpp @@ -156,6 +156,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(); @@ -338,8 +344,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 87abcb1ebbe..77c8828254f 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 881ef4f44e2..691d00cf48c 100644 --- a/src/game/client/gameclient.cpp +++ b/src/game/client/gameclient.cpp @@ -144,6 +144,7 @@ void CGameClient::OnConsoleInit() &m_Chat, &m_Broadcast, &m_DebugHud, + &m_TouchControls, &m_Scoreboard, &m_Statboard, &m_Motd, @@ -159,10 +160,11 @@ void CGameClient::OnConsoleInit() &m_GameConsole, &m_Chat, // chat has higher prio, due to that you can quit it by pressing esc &m_Motd, // for pressing esc to remove it - &m_Menus, &m_Spectator, &m_Emoticon, + &m_Menus, &m_Controls, + &m_TouchControls, &m_Binds}); // add basic console commands @@ -402,6 +404,18 @@ void CGameClient::OnUpdate() } } + // handle touch events + std::vector vTouchFingerStates = Input()->TouchFingerStates(); + bool TouchHandled = false; + for(auto &pComponent : m_vpInput) + { + TouchHandled |= pComponent->OnTouchState(vTouchFingerStates); + } + if(TouchHandled) + { + Input()->ClearTouchDeltas(); + } + // 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 60aa7f4e0b5..c5c7e9fae4c 100644 --- a/src/game/client/gameclient.h +++ b/src/game/client/gameclient.h @@ -56,6 +56,7 @@ #include "components/spectator.h" #include "components/statboard.h" #include "components/tooltips.h" +#include "components/touch_controls.h" #include "components/voting.h" class CGameInfo @@ -137,6 +138,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