Design question ‐ Entity movement on server - jmdisher/OctoberProject GitHub Wiki

An Entity is the in-game representation of a player. Notably, this means that it can move. This introduces 2 questions:

  1. How can the quasi-continuous experience of the client UI best be turned into discrete packets of meaning for the core game logic?
  2. How should the movement command be encoded such that it doesn't allow cheating but can also represent the player's intended movements as well as passive movements such as falling?

Client-side, full-tick movements, layered with other actions (OctoberProject 1.7)

Client-server considerations: Attempts to allow hybrid client-server location control have always run into issues with synchronization so, even though they avoid many forms of cheating, they should not be considered. Server-side validation must be applied in order to avoid both conflicts and client-side cheating.

Continuous/discrete considerations: Previous attempts to create one movement action per frame, during movement, required complex server-side action scheduling and also resulted in rounding errors and would cause problems with higher frame-rates on the client. This means that it would be preferable for a design to permit at max 1 action from the client, per server-side tick. Given that the default tick time is 50 ms, this shouldn't cause too much loss of movement precision. Additionally, this means that some actions need to be combined into one (for example, running and jumping), which is a new requirement.

Sketch of approach:

  • the client will need to accumulate partial movements in front of SpeculativeProjection for packaging into full 1-tick movements
  • this new movement action will need to support nested "meanwhile" actions (jump, eat, craft, break, place, etc)
  • likely, the movement action will be the only action the client can send (meaning they will often be no-ops, just to contain the meanwhile)
  • the movement action will need to describe how the movement should be interpreted (standing, walking, running, swimming) in order to determine the cost and maximum movement thresholds (meanwhile may further restrict this - for example, crafting might only be accepted while "standing" while jumping will increase the upward velocity)
  • the action will send the ending location and velocity. The location can be validated by checking if there is a path within the movement limit while velocity validation is still TBD. Velocity can't be fully determined by the server since, for example, it doesn't know if a jump was at the beginning or ending of the tick, nor what specific path the entity took (it is hit a ceiling, etc)
  • collision with blocks will be accounted for in this action
  • collision with other entities will be account for in this action

Previous thoughts/designs below...

Quasi-continuous versus discrete

The OctoberPlains client is written against LibGDX and so the obvious way to capture player commands is by sampling keyboard and mouse state at the beginning of each frame.

Those intentions, along with the frame delta, can then be converted into discrete commands within the units of local machine frame rate. These commands are then run against the client's local projection to update its state, allowing the UI to display a believable projection of this change.

However, the commands must also be sent to the server so that they can be run against the shared state. The server operates in a time frame which is in logical ticks, not specific to the frame rate of any particular machine. This means that there must be a conversion here.

Movement command encoding

The simplest way to encode movement is just to encode the final destination and have the server apply it. This, however, leaves the door open for various types of trivial cheating.

Some checks could be added, to make sure that the move is possible, but then that would require the starting position (in order to detect when a move is based on a rejected one and should be also rejected). Further, how would the check account for time?

Passive movement

An additional dimension to this problem is the matter of passive movements, such as falling. While the entity can choose not to move, they shouldn't be allowed to choose not to fall.

The server could apply this logic but that would make handling a fall, on the client-side, highly sensitive to network latency and server tick completion time.

In order to avoid this, the client must be responsible for applying the fall. This means that there is another check in the movement command to ensure that the required z-motion has been included in the result of the movement. Again, this requires accounting for time.

Current implementation

Relevant code:

The current implementation considers the following inputs:

  • previous entity location - this allows rejection of commands based on rejected commands
  • milliseconds to wait before moving - this allows handling of falling, even if the entity isn't actively moving
  • horizontal movement - the active part of the movement (also used to consider the milliseconds actively moving, to add that to the fall duration)
  • the z-vector of the entity object - used to determine if they are jumping or falling

The server's TickRunner then uses a scheduling system to enqueue entity commands for a given tick. This scheduling system checks how long the command should take, in milliseconds, and uses that to determine what can be scheduled in a single tick, potentially allowing an "in-progress" action to be held across multiple ticks.

This approach generally works but does have a few problems:

  • it means that only one command can act on a given entity at a given point in time so, for example, deciding to craft something would prevent the entity from falling. This could be addressed by making all commands handle common "passive" operations, though.
  • if the entity falls behind in how quickly it can send commands to the server, it will never be able to catch up since even "do nothing" commands take up the entire schedule (which is unfortunate since it otherwise handles being heavily out of sync very well)
  • it is generally inelegant and hard to follow as it needs to consider too many moving parts

Brainstorming other solutions

Some other ideas being considered as better long-term solutions.

Repackage movement command on client

The client currently applies the same stream of commands to its local projection as it sends to the server. While somewhat ugly, it would be possible to repackage these so that they would always be either full ticks or not sent at all.

This would also remove one of the main reasons why the server has the scheduler, at all (as the other reasons are likely to be removed in future).

Vector-based movement with 2 commands

This approach would be similar to the one currently used but would instead encode movement commands as changes to the entity's horizontal movement vectors and would then synthesize a new command to apply these to its location for each local frame or full server tick.

This would have a similar complexity to the repackaging solution, so isn't really helpful, but is listed here since we might want it in future to handle things like surface friction.

Current plan

The current plan to address the issues involves:

  • we will add a general helper for handling passive actions and call it from the active commands
  • we will not send "do nothing" movement commands from the client if they actually do nothing (neither moving nor accounting for z-movement)
  • we will limit the server-side per-entity command queue and disconnect clients which flood the server or fall too far behind