Skip to content

Input Module Planning

Tristan Grimmer edited this page Jul 14, 2025 · 17 revisions

Tacent Input Module

The next module for Tacent is going to be for controller and gamepad input.

The Input Module handles retrieving input from various devices including gamepads that may be attached. A few definitions are helpful to describe the types and nomenclature used in this module. We'll start from the top-down.

Device

A device is a piece of hardware attached to your computer that supports providing the computer with one or more forms of input. A gamepad / xbox 360 controller is a device. There is a one-to-one mapping between the specific tDevice sub-class and a physical device. The tDevice base class provides common functionality for a sub-classed tDevice type. Currently the only supported device is a 360 controller or suitable compatible devices like the 8BitDo. All instances of a tDevice have a unique identifier in case multiple are plugged in simultaneously. Devices contain one or more "components" described below.

Component

A component is one of possibly many pieces of "independent" input hardware in a device. For example, a gamepad device often has two joysticks, multiple buttons, triggers, and a d-pad. A component is basically a base class that contains a unique identifier/name for every part of a device. Some components have multiple dependent parts. For example, a joystick has two axis units, but these are controlled by a single physical 'stick' which means that, for example, dead-zone calculations need to take into account both axes to be done correctly. Units are described below.

Unit

The atom of the input world. Indivisible. An input unit is one of (usually many) pieces of hardware in a device that reads a single user input. A physical button, for example, is an input unit. So is one of the axes of a joystick. Note that a joystick is not a unit -- it is a component made with two axis units, one for each orthogonal direction. There is a one-to-one mapping between the tUnit class and a physical device that reads a single value. Units do not do any input processing so no dead-zone or anti-jitter functionality exists in units themselves -- they simply get updated/clamped and have accessors. The supported input units are described below.

Summary

In short, a device is made up of components, and components are made up of units.

  • Devices are uniquely identified and have names.
  • Components are uniquely defined within a device and have names.
  • Units are uniquely defined within a component. All three exist in the tInput namespace and have base classes: tDevice, tComponent, and tUnit.

Sub classes start with the following prefixes to identify them:

  • td : Tacent Input Device.
  • tc : Tacent Input Component.
  • tu : Tacent Input Unit.

Supported Units

  • tuBoolState A binary unit that is either on or off. Think buttons and switches. No intermediate states. The tuBoolState has a current state boolean member, a polling state boolean member, as well as an event queue of state changes to be processed which when applied will bring the current state 'up to date' with the polling (raw) state.

  • tuMultiState
    An input unit with multiple input states. Things like a button that responds to different pressure levels. The tuMultiState has a current state enum member, a polling state enum member, as well as an event queue of state changes to be processed which when applied will bring the current state 'up to date' with the polling (raw) state.

  • tuAxis
    An input unit that contains a value in [-1.0, 1.0]. Things like a trigger are made of one of these. Things like a joystick are made of two of these.

  • tuDisplacement
    An input unit that reports a value in [0.0, 1.0]. Displacement units do not return to any default/resting position if untouched. May need anti-jitter to be implemented.

Supported Components

  • tcButton
    Contains a single tuBoolState. Buttons always return to their default off state when not physically held down.

  • tcSwitch
    Contains a single tuBoolState. Switches remain in the state they're set to last. On or off.

  • tcDirectionPad
    D-Pads are made of dependent tuBoolState units, one for each or the 4 cardinal directions. They are dependent because they are mechanically restricted from all being pressed at the same time. Typically only one or two of the 4 direction buttons may be simultaneously engaged (at 45 degrees some support 2). They all return to the off state if no physical input.

  • tcTrigger
    Made up of a single tuDisplacement. Dead-zone implemented for values close to zero, plus optional anti-jutter. Returns to 0 if no physical input applied.

  • tcPedal
    Made up of a single tuDisplacement. Dead-zone implemented for values close to zero, plus optional anti-jutter. Returns to 0 if no physical input applied.

  • tcJoystick
    A joystick contains one tuAxis for the horizontal (X) direction, and another for the vertical (Y) direction. The axes of a joystick are NOT independent since the same physical stick displaces both X and Y magnets for the hall effect transistors to pick up. This class implements dead-zone and anti-jitter correctly as it can read both axes. It's 'resting' position is 0.0 and is unstable due to the small physical forces (often springs) keeping it at 0. No physical input and a joystick returns to 0.0 for both X and Y axes.

  • tcDial
    Made up of a single tuDisplacement. Dials do not return to 0. Implement anti-jitter but no dead zone. Used for things like volume control knobs.

  • tcSlider
    Made up of a single tuDisplacement. Sliders do not return to 0. Implement anti-jitter but no dead zone.

Device Manager

The device manager (tDeviceManager) is responsible for, well, managing devices. It allows the client to register callbacks when important things happen like the addition of a new device or the removal of an existing device. Querying the attached devices is done on a separate 'polling' thread.

Polling Thread

The polling thread runs at a high frequency -- user configureable but hundreds or even 1000Hz is not unreasonable. In this polling thread the device hardware is read using system APIs. In particular xinput on Windows, and the fcntl/joystick APIs on Linux. The polling thread then updates the units for the device in question. For units with 'state' in their name, the polling (raw) state is updated and an event is added if the polling state has changed from the previous poll. This allows things like button presses and releases to 'not be missed' if they happen so quickly that they are within a single main thread update. Updating of the input units, including event queues, are protected by mutexes.

Main Thread

Main thread updates are typically much slower -- in the order of 60Hz. The client of tDeviceManager is responsible for registering callbacks that it cares about (like device connection/disconnection) on construction (which starts up the polling thread), calling an 'Update' function periodically (usually at the current framerate), and reading any values from the devices it cares about. The destructor will automatically perform any required shutdown including stopping the polling thread.

Any connection/disconnection callbacks are called in the context of the main thead when the Update function is called. The update function performs the following operations:

  • Process a single event on any units that have the word 'state' in them. It is important that only one is done at a time so the main loop can respond to any changes. Over multiple frames it will eventually 'catch up' to the polling state. This ensures the main thread has a chance to react to any 'twitchy' state changes that were quicker than 33 or 16 milliseconds in duration.
  • Process any dead-zones for all tuAxis and tuDisplacement units.
  • Process anti-jitter for all tuAxis and tuDisplacement units. After the update call, the client is free to read any component of any device it feels like directly. The read calls of all the components also use mutexes to ensure consistent data and no collisions with updates from the polling thread.

Note that we could do things like dead-zone/anti-jitter calculations in the polling thread at the higher frequency but since there are no 'state changes' to manage, the result would be the same. It therefore makes sense to do these in the main thread update call. The polling thread should be as fast and efficient as possible. The main thread update also cares about 'components' and the dependencies between the different units. In the joystick component, for example, the update call knows that the two tuAxis units are dependent on each other and can make the dead-zone a proper circle rather than a square. A square is what you'd get if the axes were treated independently. The polling thread also cares about 'components'. It is important that all the units of a component get updated 'atomically'. This keeps things like the two axes perfectly in sync with each other when the main thread update does it's work.

Table Of Contents

Clone this wiki locally