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 different buttons are only shown when they are usable depending on the context. The touch button configuration is stored in `touch_controls.json`. Closes ddnet#8359.

Default touch button configuration
----------------------------------

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 center of the screen.

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 mode can be adjusted separately for ingame/spectating, to prevent accidental direct touch input when using a joystick.

When spectating, direct touch input is used to allow panning the map directly like in an image/map viewer. Virtual joysticks can also be used to pan the map while spectating.

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. This includes buttons for showing the scoreboard, showing the emoticon selector, showing the spectator menu, opening team and team chat, voting yes/no, and zooming. Long pressing the hamburger menu button will open the ingame menu.

When the dummy is connected, a button for swapping between main and dummy is shown.

The emoticon selector and spectator menu 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 menu.

Touch button configuration format
---------------------------------

The default button layout described above is loaded from `data/touch_controls.json`. This layout can be overridden by creating `touch_controls.json` in the user directory.

The attribute `"touch-buttons"` of the root object in this file specifies an array of touch button objects. Each touch button object has the following adjustable properties:

- Position and size (attributes `x`, `y`, `w`, `h`): the X/Y position and width/height are integers on a 1,000,000² grid. The unit grid values are converted to screen grid values at runtime in relation to the aspect ratio of the screen. This means buttons may appear stretched if the resolution is changed, but it allows us to provide a reasonable default for slightly different aspect ratios. If we save the absolute position instead, we would have the problem of buttons appearing outside the screen or not being aligned on different aspect ratios.
- Shape (attribute `shape`): determines the shape of the button being rendered.
    - `"rect"`: rectangle shape.
    - `"circle"`: circle shape. The button size will automatically be adjusted so that width and height are identical.
- Visibility (attribute `visibilities`): an array of predefined visibility classes can be selected and the button is only shown if all conditions are satisfied. An empty array means that the button is always shown. The following visibility classes are defined:
    - `"ingame"`: player is ingame, i.e. not spectating.
    - `"extra-menu"`, `"extra-menu-2"`, ..., `"extra-menu-5"`: the extra menu with the given number is activated.
    - `"zoom-allowed"`: zoom is allowed on this server.
    - `"vote-active"`: a vote is currently active.
    - `"dummy-allowed"`: dummy is allowed on this server.
    - `"dummy-connected"`: dummy is currently connected.
    - `"rcon-authed"`: player is currently authed in rcon.
    - `"demo-player"`: demo player is currently active.
    - All visibility classes can be inverted by prefixing them with `-`, e.g. `"-ingame"` is satisfied when the player is not ingame, i.e. spectating.
- Behavior (attribute `behavior`): an object which describes the behavior of this touch button when it is activated/deactivated as well as its label. The attribute `type` is used to differentiate which type of behavior is used. The behavior is either predefined (hard-coded) or a generic console command (bind). Predefined behavior is only used where necessary, all other buttons are represented as generic binds.
    - Predefined behavior (attribute `type` set to `"predefined"`): The attribute `id` is set to a fixed string value which determines the specific predefined behavior. The following predefined behaviors exist:
        - `"ingame-menu"`: Opens the ingame menu immediately when released.
        - `"extra-menu"`: The extra menu button which toggles visibility of buttons with `"extra-menu"`, `"extra-menu-2"`, `"extra-menu-3"` etc. visibilities. Also opens the ingame menu on long press.
            - The attribute `"number"` specifies an integer between 1 and 5 to associate this button with the respective visibilities `"extra-menu"`, `"extra-menu-2"`, ..., `"extra-menu-5"`.
        - `"emoticon"`: Opens the emoticon selector (this does not work with binds).
        - `"spectate"`: Opens the spectator menu (this does not work with binds).
        - `"swap-action"`: Swaps the active action (fire and hook) for direct touch input and virtual joysticks.
        - `"use-action"`: Uses the active action with the current aiming position.
        - `"joystick-action"`: Virtual joystick which uses the active action.
        - `"joystick-aim"`: Virtual joystick which only aims without using an action.
        - `"joystick-fire"`: Virtual joystick which always uses fire.
        - `"joystick-hook"`: Virtual joystick which always uses hook.
    - Bind behavior (attribute `type` set to `"bind"`). Buttons with this behavior work like regular key binds.
        - The attribute `"label"` specifies as a string the label of the button.
        - The attribute `"label-type"` specifies as a string the type of the label of the button, i.e. how the attribute `"label"` is interpreted:
            - `"plain"`: Label is used as is.
            - `"localized"`: Label is localized. Only usable for the default buttons for which there are translations available.
            - `"icon"`: Icon font is used for the label. Icons must be encoded using `\uXXXX`, e.g. `\uf3ce` for the mobile phone icon.
        - The attribute `"command"` specifies as a string the command to execute in the console like a bind when this button is used.

The root object additionally has the following attributes:

- `"direct-touch-ingame"`: specifies the mode of direct touch input while ingame. Possible values:
    - `"disabled"`: Direct touch input is not used while ingame. This means a virtual joystick is necessary.
    - `"action"`: Direct touch input uses the active action (see above).
    - `"aim"`: Direct touch input only changes the aiming position without using an action. This means separate buttons for using the actions are necessary.
    - `"fire"`: Direct touch input always uses fire.
    - `"hook"`: Direct touch input always uses hook.
- `"direct-touch-spectate"`: specifies the mode of direct touch input while ingame. Possible values:
    - `"disabled"`: Direct touch input is not used while spectating. This means a virtual joystick is necessary.
    - `"aim"`: Direct touch input is used for spectating.

Ingame menu buttons
-------------------

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:

- Buttons to open the local and remote consoles. Opening the local console without the touch controls is useful because error messages would be shown there if the touch controls configuration could not be loaded.
- Button to close the menu, which is more convenient than using the virtual back button if it's not always shown.
- Checkbox for toggling the touch controls editor UI (see below).

Ingame touch controls editor
----------------------------

The user interface to adjust the touch controls is rendered over the main screen of the ingame menu when enabled. For now, the implementation of the touch controls editor is limited to basic configuration management functions.

- Saving the configuration to the `touch_controls.json` file in the config directory.
- Discarding the current changes by reloading the `touch_controls.json` file from the config directory.
- Restoring the default configuration by reloading the `touch_controls.json` file from the data directory.
- Displaying whether there are unsaved changes.
- Importing and exporting the configuration from and to the clipboard. This is the only way to edit the configuration on newer Android versions, as editing files within apps' storage is not possible anymore.

Furthermore, the global touch controls settings can be adjusted in this UI:

- The direct touch input modes while ingame and spectating (see Touch button configuration format) can be adjusted using dropdown menus.

While the touch controls editor is active, all touch buttons are shown regardless of their visibilities, to better support arranging the buttons.

Future tasks and ideas
----------------------

- Implement more usable UI for adjusting the button layout.
- Add `color` property to touch buttons to adjust the default background color?
- Add `choice` predefined behavior which shows a selection popup for 2 or more other behaviors?
- Add combined weapon picker button that shows all currently available weapons.
- Support zooming gesture while spectating.
- Optimize touch button rendering using text and quad containers.
  • Loading branch information
Robyt3 committed Oct 2, 2024
1 parent b80b55c commit 25f93ed
Show file tree
Hide file tree
Showing 10 changed files with 2,524 additions and 14 deletions.
3 changes: 3 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1846,6 +1846,7 @@ set(EXPECTED_DATA
themes/winter.png
themes/winter_day.map
themes/winter_night.map
touch_controls.json
wordlist.txt
)

Expand Down Expand Up @@ -2402,6 +2403,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
311 changes: 311 additions & 0 deletions data/touch_controls.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,311 @@
{
"direct-touch-ingame": "action",
"direct-touch-spectate": "aim",
"touch-buttons": [
{
"x": 0,
"y": 833333,
"w": 200000,
"h": 166667,
"shape": "rect",
"visibilities": [
"ingame"
],
"behavior": {
"type": "bind",
"label": "Move left",
"label-type": "localized",
"command": "+left"
}
},
{
"x": 200000,
"y": 833333,
"w": 200000,
"h": 166667,
"shape": "rect",
"visibilities": [
"ingame"
],
"behavior": {
"type": "bind",
"label": "Move right",
"label-type": "localized",
"command": "+right"
}
},
{
"x": 100000,
"y": 666667,
"w": 200000,
"h": 166667,
"shape": "rect",
"visibilities": [
"ingame"
],
"behavior": {
"type": "bind",
"label": "Jump",
"label-type": "localized",
"command": "+jump"
}
},
{
"x": 116667,
"y": 16667,
"w": 83333,
"h": 83333,
"shape": "rect",
"visibilities": [
"ingame"
],
"behavior": {
"type": "bind",
"label": "Prev. weapon",
"label-type": "localized",
"command": "+prevweapon"
}
},
{
"x": 200000,
"y": 16667,
"w": 83333,
"h": 83333,
"shape": "rect",
"visibilities": [
"ingame"
],
"behavior": {
"type": "bind",
"label": "Next weapon",
"label-type": "localized",
"command": "+nextweapon"
}
},
{
"x": 16667,
"y": 16667,
"w": 83333,
"h": 83333,
"shape": "rect",
"visibilities": [
],
"behavior": {
"type": "predefined",
"id": "extra-menu",
"number": 1
}
},
{
"x": 300000,
"y": 16667,
"w": 83333,
"h": 83333,
"shape": "rect",
"visibilities": [
"extra-menu",
"zoom-allowed"
],
"behavior": {
"type": "bind",
"label": "Zoom out",
"label-type": "localized",
"command": "zoom-"
}
},
{
"x": 383333,
"y": 16667,
"w": 83333,
"h": 83333,
"shape": "rect",
"visibilities": [
"extra-menu",
"zoom-allowed"
],
"behavior": {
"type": "bind",
"label": "Default zoom",
"label-type": "localized",
"command": "zoom"
}
},
{
"x": 466666,
"y": 16667,
"w": 83333,
"h": 83333,
"shape": "rect",
"visibilities": [
"extra-menu",
"zoom-allowed"
],
"behavior": {
"type": "bind",
"label": "Zoom in",
"label-type": "localized",
"command": "zoom+"
}
},
{
"x": 16667,
"y": 133333,
"w": 83333,
"h": 66667,
"shape": "rect",
"visibilities": [
"extra-menu"
],
"behavior": {
"type": "bind",
"label": "Scoreboard",
"label-type": "localized",
"command": "+scoreboard"
}
},
{
"x": 116667,
"y": 133333,
"w": 83333,
"h": 66667,
"shape": "rect",
"visibilities": [
"ingame",
"extra-menu"
],
"behavior": {
"type": "predefined",
"id": "emoticon"
}
},
{
"x": 116667,
"y": 133333,
"w": 83333,
"h": 66667,
"shape": "rect",
"visibilities": [
"-ingame",
"extra-menu"
],
"behavior": {
"type": "predefined",
"id": "spectate"
}
},
{
"x": 216667,
"y": 133333,
"w": 83333,
"h": 66667,
"shape": "rect",
"visibilities": [
"extra-menu",
"-demo-player"
],
"behavior": {
"type": "bind",
"label": "Chat",
"label-type": "localized",
"command": "chat all"
}
},
{
"x": 316667,
"y": 133333,
"w": 83333,
"h": 66667,
"shape": "rect",
"visibilities": [
"extra-menu",
"-demo-player"
],
"behavior": {
"type": "bind",
"label": "Team chat",
"label-type": "localized",
"command": "chat team"
}
},
{
"x": 16667,
"y": 333333,
"w": 83333,
"h": 66667,
"shape": "rect",
"visibilities": [
"extra-menu",
"vote-active",
"-demo-player"
],
"behavior": {
"type": "bind",
"label": "Vote yes",
"label-type": "localized",
"command": "vote yes"
}
},
{
"x": 116667,
"y": 333333,
"w": 83333,
"h": 66667,
"shape": "rect",
"visibilities": [
"extra-menu",
"vote-active",
"-demo-player"
],
"behavior": {
"type": "bind",
"label": "Vote no",
"label-type": "localized",
"command": "vote no"
}
},
{
"x": 766667,
"y": 16667,
"w": 100000,
"h": 100000,
"shape": "rect",
"visibilities": [
"dummy-connected"
],
"behavior": {
"type": "bind",
"label": "Toggle dummy",
"label-type": "localized",
"command": "toggle cl_dummy 0 1"
}
},
{
"x": 883333,
"y": 16667,
"w": 100000,
"h": 100000,
"shape": "rect",
"visibilities": [
"ingame"
],
"behavior": {
"type": "predefined",
"id": "swap-action"
}
},
{
"x": 755000,
"y": 580000,
"w": 225000,
"h": 400000,
"shape": "circle",
"visibilities": [
"ingame"
],
"behavior": {
"type": "predefined",
"id": "joystick-action"
}
}
]
}
5 changes: 5 additions & 0 deletions src/engine/shared/config_variables.h
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ 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(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
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
7 changes: 6 additions & 1 deletion src/game/client/components/menus.h
Original file line number Diff line number Diff line change
Expand Up @@ -476,8 +476,12 @@ class CMenus : public CComponent
// found in menus_ingame.cpp
STextContainerIndex m_MotdTextContainerIndex;
void RenderGame(CUIRect MainView);
void RenderTouchControlsEditor(CUIRect MainView);
void PopupConfirmDisconnect();
void PopupConfirmDisconnectDummy();
void PopupConfirmDiscardTouchControlsChanged();
void PopupConfirmResetTouchControls();
void PopupConfirmImportTouchControlsClipboard();
void RenderPlayers(CUIRect MainView);
void RenderServerInfo(CUIRect MainView);
void RenderServerInfoMotd(CUIRect Motd);
Expand Down Expand Up @@ -638,7 +642,6 @@ class CMenus : public CComponent
static CUi::EPopupMenuFunctionResult PopupMapPicker(void *pContext, CUIRect View, bool Active);

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

IGraphics::CTextureHandle m_TextureBlob;
Expand All @@ -658,6 +661,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 25f93ed

Please sign in to comment.