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.

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`.
  • Loading branch information
Robyt3 committed Aug 11, 2024
1 parent afd1bd9 commit 88b1b28
Show file tree
Hide file tree
Showing 20 changed files with 955 additions and 57 deletions.
2 changes: 2 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
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 @@ -303,6 +303,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 @@ -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
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 @@ -150,6 +150,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 @@ -136,6 +136,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
80 changes: 56 additions & 24 deletions src/game/client/components/emoticon.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ void CEmoticon::OnReset()
m_Active = false;
m_SelectedEmote = -1;
m_SelectedEyeEmote = -1;
m_TouchPressedOutside = false;
}

void CEmoticon::OnRelease()
Expand All @@ -58,13 +59,30 @@ 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)
return;

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)
Expand All @@ -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;

Expand All @@ -96,35 +139,29 @@ 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();

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();
}
Expand All @@ -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)
Expand Down
8 changes: 8 additions & 0 deletions src/game/client/components/emoticon.h
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
#define GAME_CLIENT_COMPONENTS_EMOTICON_H
#include <base/vmath.h>
#include <engine/console.h>

#include <game/client/component.h>
#include <game/client/ui.h>

class CEmoticon : public CComponent
{
Expand All @@ -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);

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

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

IGraphics::CTextureHandle m_TextureBlob;
Expand All @@ -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;
Expand Down
Loading

0 comments on commit 88b1b28

Please sign in to comment.