Skip to content

Commit

Permalink
Add ingame touch controls
Browse files Browse the repository at this point in the history
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.

The button to switch weapons uses swiping-based controls. Swiping left/right will switch to the previous/next weapon, respectively. Pressing the button shortly without significant movement will always switch to the next weapon. Simply showing two separate buttons to switch to the previous and next weapons would also be possible.

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 connecting the dummy, showing the scoreboard, showing the emoticon HUD (not implemented yet), showing the spectator HUD (not implemented yet), opening team and team chat, voting yes/no, and zooming. Once the dummy is connected, the button for swapping between active and dummy is always shown.

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:

- Opening and using the Emoticon HUD.
- Opening and using the Spectator HUD.
- Various decisions and code cleanup TODOs in `CTouchControls`.
  • Loading branch information
Robyt3 committed Jul 26, 2024
1 parent 08f955b commit d546ad9
Show file tree
Hide file tree
Showing 16 changed files with 804 additions and 28 deletions.
2 changes: 2 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
13 changes: 9 additions & 4 deletions src/engine/client/input.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,14 @@ const std::vector<IInput::CTouchFingerState> &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);
Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions src/engine/client/input.h
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@ class CInput : public IEngineInput
bool NativeMousePressed(int Index) const override;

const std::vector<CTouchFingerState> &TouchFingerStates() const override;
void ClearTouchDeltas() override;

const char *GetClipboardText() override;
void SetClipboardText(const char *pText) override;
Expand Down
6 changes: 6 additions & 0 deletions src/engine/input.h
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,12 @@ class IInput : public IInterface
* @return vector of all touch finger states
*/
virtual const std::vector<CTouchFingerState> &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;
Expand Down
6 changes: 6 additions & 0 deletions src/engine/shared/config_variables.h
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
8 changes: 8 additions & 0 deletions src/game/client/component.h
Original file line number Diff line number Diff line change
Expand Up @@ -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<IInput::CTouchFingerState> &vTouchFingerStates) { return false; }
};

#endif
2 changes: 1 addition & 1 deletion src/game/client/components/controls.h
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand Down
3 changes: 2 additions & 1 deletion src/game/client/components/menus.h
Original file line number Diff line number Diff line change
Expand Up @@ -592,7 +592,6 @@ class CMenus : public CComponent
void RenderSettingsCustom(CUIRect MainView);

void SetNeedSendInfo();
void SetActive(bool Active);
void UpdateColors();

IGraphics::CTextureHandle m_TextureBlob;
Expand All @@ -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;
Expand Down
75 changes: 55 additions & 20 deletions src/game/client/components/menus_ingame.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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))
{
Expand All @@ -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();
Expand Down Expand Up @@ -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))
Expand All @@ -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))
{
Expand All @@ -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))
Expand All @@ -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))
Expand All @@ -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))
{
Expand All @@ -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;
Expand All @@ -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()
Expand Down
Loading

0 comments on commit d546ad9

Please sign in to comment.