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.
Why are player controls being reworked?
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.
Let's examine how input events are translated into the logical player controls that
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:
The following formula can be utilized to apply the appropriate change to the relevant player data.
newValue = oldValue + offset + velocity * elapsedTime
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 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.”)
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.
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.
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
The game is responsible for defining the appropriate acceleration factors and thresholds for keys and other axes.
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.
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.
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.
(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:
D_NetDamageMobj()) when they think they've hit a mobj.