Table of Contents

Player controls

This article describes the system of how input events are used for controlling players, and how this works in the client/server architecture that Doomsday is using.

Motivation

Why are player controls being reworked?

How players are controlled?

This diagram illustrates how input events are used to control players. The server receives two things:

It is noteworthy that the old concept of ticcmds is no longer needed in this system.

We are focusing on providing a latency-free client-side experience, which may on occasion be less than accurate, but still close enough to the real situation. For instance, the server will execute player actions (such as firing a weapon or opening a door) at the coordinates which the client was really using at the time when the action was made: the client will see the result that he expects, although due to network latency the results of the action may be delayed slightly.

When it comes to player movement, it is entirely up to the client: only the client knows the exactly correct latency-free position of the local players. The server will respect this information and apply necessary safeguards against cheating when the movement information is received from the clients.

The server remains the referee on deciding who succeeds in damaging whom. Clients will send a damage request when they think they have hit a specific mobj; the server can then determine if the request is legal. This way clients are able to hit targets even though there is some latency and the target is moving.

From input events to ''P_PlayerThink''

Let's examine how input events are translated into the logical player controls that P_PlayerThink() uses.

Overview

  1. During init, game registers logical controls.
  2. Engine allows input devices to be bound to the logical controls.
  3. Engine updates the state of the logical controls based on input events.
  4. On every tick/frame, game checks the logical controls and updates its state (game does not know how the values were produced). Game can apply additional acceleration for the “speed” control, for example.

Game-side: logical game controls

P_PlayerThink() deals in terms of logical controls, which tell it what kind of actions the player is currently undertaking. All of this is happening on game-side. When the thinker queries the state of a particular logical control, say “turn” (which dictates how the player look direction changes around the Z axis), it will receive both the current velocity of the logical control (in logical units per second), as well as an additional offset (in logical units).

Why two values? The same control may be affected both by an absolute and a relative device axis, and these must be applied separately, as the value of the absolute axis needs to be multiplied with the current elapsed time, whereas relative axes need no such provision.

Logical controls are applied to two kinds of player properties:

Applying a logical control to an absolute property

The following formula can be utilized to apply the appropriate change to the relevant player data.

newValue = oldValue + offset + velocity * elapsedTime

Applying a logical control to a 1-D vector property

The following formula should be used when determining the current value:

newValue = clamp( F * offset + velocity )

Where F is a sensitivity factor that defines how strongly the values from the device influence the 1-D vector.

Numeric and impulse controls

Numeric logical controls are all of the same type regardless of how they're used by the game: they all evaluate into floating-point velocities and offsets. No distinction should be made between axis controls and toggle controls at the logical control level, which is what the game registers into the engine at init time. The type of the devices bound to those logical controls determines the ultimate behavior of the logical controls.

It should be noted that inputs based on absolute or relative axes are closely coupled with logical controls: only changes in the input device axis itself will show up as a change in the logical control. (This is called an “axis binding.”)

Impulse controls, on the other hand, can be triggered multiple times before they're handled. There is a buffer which holds the unhandled impulses, until the game is ready to process them. Impulse controls are triggered via console commands, so they can be created anywhere, and at any time. Therefore, there is only a loose coupling between actual input devices and logical controls of the impulse variety. (Regular “command binding.”)

Engine-side: device axes, keys, and the impulse buffer

The engine-side code must be able to respond to PlayerThink's query about the velocity and offset affecting a particular logical control of a specific player. These are composed out of multiple sources of data, i.e., all the device axes and the key/button states.

In order to do this, the engine will need to consult the axis bindings that connect device axes with a specific player's logical control(s), and the toggle states, which have been updated by the console when a bound command is executed.

The control code should not need to know about the axis bindings. The bindings management updates the status of the axes, so that the control code can just check those.

Binding classes for device bindings

Each device state is only usable in the highest active binding class in which it is bound. For example, let us say that the “automap” class uses the Left key for map panning. The “game” class uses the Left key also, but for turning the player to the left. If the automap class is active, the state of the Left key is associated with the automap class (it being the highest active one), and the turn control will see the state of the Left key as zero.

In other words, each device state (key, button, axis position) keeps track of the highest active binding class where the device state is being used. These associations need to be updated only when a class is activated or deactivated. When the device state is read for a particular control, the class of that control determines whether the reading is successful.

Time-based acceleration

The engine applies time-based velocity acceleration for key-bound controls. When the key is pressed, a timestamp is stored. When the acceleration threshold is exceeded, the appropriate acceleration factor is then applied.

Why does the engine needs to do this? Consider the case where an axis and keys are simultaneously bound to the “turn” control. The keys should be affected by the time-based acceleration, while the axis should not. (If the axis is a digital one, e.g., in a gamepad, it should be treated as buttons instead.) The game still expects to receive one offset value and one velocity value for the logical control in P_PlayerThink().

The game is responsible for defining the appropriate acceleration factors and thresholds for keys and other axes.

Additional acceleration

While the Speed control is held, the game should apply an additional acceleration factor to the values received from the engine, when it's handling the controls that are affected by the acceleration. Note that, e.g., the offset value of the “turn” control is not accelerated by Speed.

Impulses

In addition, the engine-side code must be able to return all the impulses, one by one, from the FIFO buffer where they are being stored for processing. The impulses are added to the buffer by the console, so the control management does not need to worry about where they actually come from.

Bindings

At the lowest level, on engine-side, input events are handled by the bindings responder, and the console commands bound to the events are executed.

TODO

(for version 1.9.0-beta6)

The mechanism described in this article of how players are controlled is not fully implemented at the moment. These are the things we need to do:

Goal 3: Unified Networking Model

Goal N

Goal N+1: Player Control Setup GUI

Already done

Goal 2

Goal 1: Player controllable locally