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.
- 1 Motivation
- 2 How players are controlled?
- 3 From input events to P_PlayerThink
- 3.1 Overview
- 3.2 Game-side: logical game controls
- 3.3 Engine-side: device axes, keys, and the impulse buffer
- 3.4 The bindings
- 4 TODO
- 5 Already done
Why are player controls being reworked?
- Engine-side control management. Games just have to query the control state.
- Preparing for the unified networking model where there is no separation between single-player and multiplayer, and ticcmds are no longer used.
- Support for multiple local players.
- Flexible (engine-side) configuration of input device axes to player controls.
How players are controlled?
This diagram illustrates how input events are used to control players. The server receives two things:
- Periodic position, velocity, and direction information from the players (these don't have to have the same frequency, for instance the absolute position could be sent less frequently). This is generic information that cannot be modified by games.
- Impulses at a specific location and player state, i.e., action requests. Games can introduce new impulses, because the impulses are handled by the games (the engine is not aware of these even occurring).
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.
- During init, game registers logical controls.
- Engine allows input devices to be bound to the logical controls.
- Engine updates the state of the logical controls based on input events.
- 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:
- Absolute: for example, the view angles.
- 1-D Vector: for example, movement speeds for front, right, and up axes (values between -1 and 1). But also, all toggles like attack, use, speed, jump (values between 0 and 1).
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.
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.
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:
Goal 3: Unified Networking Model
- Only clients are allowed to play the game. Trying to play as a "server player" or the "single player" must cause a fatal error.
- Local execution of commands, netgames (and unified games, soon to come):
- Clients are not able to damage their mobjs locally.
- Psprite animations and sounds need to be played on the client, but any mobjs spawned by the weapon are sent from the server (will have a latency). Otherwise, it would be difficult for the server to determine which mobjs should be sent to which clients. We could make an exception for missile weapons, so that the player experience is not negatively impacted by latency: the missile mobjs spawned by the weapon could be spawned immediately by the client on client-side, but when the missile hits something it would simply be removed without any further effects. The server would then provide the rest of the effects. (It's a compromise.)
- Check that clients are sending the appropriate damage requests (D_NetDamageMobj()) when they think they've hit a mobj.
- Server should pass the player pos/mov/dir info along to other clients so that they can do the full movement prediction on their own (the goal is to try to make the remote players move similarly as local ones; smoothly and without sudden warping).
- Player movement interpolation based on pos/mov/dir info (could be linear at first, then spline-based at a later point in time, if necessary).
Goal X+1: Player Control Setup GUI
- Doomsday control panel UI for setting up the controls.
- Page for control bindings: something list-based, with multiple columns? Needs some organization into groups like "Movement", "Inventory", etc. The groups are defined when the controls are registered in the .cfg file.
- Page for axis settings (incl. mouse XY): sensitivity, smoothing, visual representation of the current axis position (like in many racing games).
- Bindings saved/restored appropriately when exiting/launching.
Goal 1: Player controllable locally
- Define control defaults for multiple players (as many players as can sensibly have defaults, 2 shouldn't be a problem at least) during init.
- The engine-side control state management needs to conform to the original behavior: e.g., slow-speed and fast-speed turning depending on how long the control is held. Same for all axes that are modified by on/off controls.
- PlayerThink needs to directly query control state and determine what the player is supposed to be doing: including movement, looking, all triggered impulses, etc. Impulses are executed as appropriate (might be more than one), with the appropriate action requests sent if necessary.
- PlayerThink and its subordinates must execute all actions dictated by the controls locally, immediately.