michalooo / Thesis-Deterministic-Lockstep-Netcode-DOTS-with-Validation

Mitigating network latency in fast-paced multiplayer online games
0 stars 0 forks source link

Project Title

Original Thesis Name: "Scalability and Democratization of Real Time Strategy and Fighting Games: The Deterministic Lockstep Netcode Model with Client Prediction Combined with the Data Oriented Tech Stack."

Current Name: "Deterministic Lockstep Netcode Model with Determinism validation and debugging tooling for Unity DOTS"

Overview

The repository contains the source code for the package made for Unity DOTS which allows for creating games which utilize deterministic lockstep netcode model, validate determinism in such game, as well as provide nondeterminism debugging tooling. The repository also provides code and assets of a sample Pong game that demonstrates the usage of the package.

Tool was implemented using Unity's Data-Oriented Technology Stack (DOTS). It needs to be noted that currently this is a showcase of methodology rather than fully working solution which will be a matter of further development.

Features provided by the package

Getting Started

These instructions will get you a copy of the project up and running on your local machine for development and testing purposes.

Prerequisites

Installation

  1. Clone the Repository:

    git clone https://github.com/michalooo/Thesis-Deterministic-Lockstep-Netcode-DOTS-with-Validation.git
  2. Open in Unity:

    • Open the project in Unity 6000.0.0.b16
  3. Download necessary packages:

    • The project already contains modified version of com.unity.entities@1.2.3
    • Download any other packages which are listed in the project`s "manifest.json"
  4. Start the sample:

    • In the editor, navigate to Assets/Pong/Scenes and open "PongMenu" scene
    • Click "Play" button in the editor to start the game

Sample Game Description

The sample game provided in the package is a modified version of the classic Pong. Two players compete by controlling paddles to hit a ball back and forth. The game starts by spawning one ball per tick until 1000 balls are spawned. The game counts the score, and when it is over, the game stops, waits for 5 seconds, and then returns to the menu.

The following elements were implemented for the sample:

Entities

  1. BallSpawner: A ball spawner entity is created in the scene and positioned in the middle of the game field. It has an authoring component to which the ball Prefab is attached, which is later used by the BallSpawnSystem to reference and spawn it. The ball will be spawned by the BallSpawnSystem later, which will use the ball spawner entity position and spawn the ball Prefab there.

  2. PlayerSpawner: Similarly to the BallSpawner entity, the PlayerSpawner entity is also created in the subscene and has an authoring component with PongPlayerSpawner component where a player Prefab is attached. It is used by the PlayerSpawnSystem to spawn players at the start of the game while referencing the player prefab.

  3. Ball: Ball entites are created once per simulation tick by the BallSpawnSystem. They are spawned in the position of a ball spawner entity, transforming into the ball Prefab into the ball Prefab assigned to it. Then the velocity component value is set, and the entity is considered spawned.

  4. Player: Two player entities are created at the beginning of the game via the PlayerSpawnSystem, transforming the created player Prefab into an entity. They are created at predefined positions on each side of the game scene.

Components

  1. Velocity: The velocity component is used to indicate the ball’s speed and direction in the form of a float3 value. This component is assigned to an entity when the Prefab is created as an entity by the spawner. The value of this component is set randomly on entity creation to randomise the speed and direction of the ball with constraints of maximum and minimum speed and angle.

  2. PlayerInput: PlayerInput is a singleton component which for now is implemented inside of the package and the future work will include the separation of it from the package. It holds the current player input, which, in the case of the Pong game, is represented by a VerticalInput float value indicating the direction and magnitude of the player’s movement. In particular, PlayerInput does not store raw input (such as key presses like ”w”), but rather the transformed input that signifies the exact action.

  3. PongBallSpawner: PongBallSpawner component contains ball entity object to spawn and its baked automatically into BallSpawner Entity.

  4. PongPlayerSpawner: Similarly to PongBallSpawner component, PongPlayerSpawner component contains player entity object to spawn and its baked automatically into player spawner entity.

  5. Player: The Player component is a tag component used to mark entities that represent players. This is important for querying purposes, as otherwise, the player entities would only have a Transform component assigned, which is insufficient for creating player-specific queries.

Systems

  1. PlayerSpawnSystem: Responsible for filtering the PongPlayerSpawner component, which exists as a single instance in the scene. This system spawns two players at the beginning of the game by querying the PlayerSpawner component and retrieving the associated entity. After the initial player spawn, the system is disabled as no additional player spawns are required.

  2. InputGatherSystem: Runs before the PlayerMovementSystem in each frame, detecting and interpreting player key presses to populate the PlayerInput component. This system queries the PlayerInput component, a singleton component with only one instance in the game, and updates its value. In the sample Pong game, the "w" and "s" inputs are interpreted as values of 1 or -1 for the verticalInput in this component, indicating player movement direction.

  3. PlayerMovementSystem: Updates player positions based on the PlayerInput component. It queries for player and transform components and moves players up or down according to the input values, multiplied by deltaTime to ensure consistent movement. Both players move locally as the base game is not yet multiplayer.

  4. BallSpawnSystem: Filters the PongBallSpawner component, of which only one instance exists in the scene, to spawn one ball per frame until 1000 balls have been spawned. It queries the BallSpawner component, retrieves the associated entity, and spawns a ball at the spawner’s position. The system then randomly sets the velocity component of the spawned ball entity.

  5. BallMovementSystem: Moves the balls every frame according to their Velocity component values. This system filters for transform and velocity components (present on ball entities) and utilizes parallel jobs for efficiency due to the large number of balls. It creates a query to gather entities with LocalTransform and Velocity components, stores specific components in temporary arrays, and updates ball positions through a parallel job (BallMovementJob). The job uses EntityCommandBuffer.ParallelWriter to ensure safe parallel updates, calculating new positions based on velocity and deltaTime. After the job completes, the command buffer is played back to apply changes, and temporary arrays are disposed of to free up memory.

  6. BallBounceSystem: Checks for ball collisions with walls or players. It has two queries: one for entities with velocity and transform components, and another for entities with transform and player components. Using these queries, the system updates the ball’s velocity component to reflect bounces upon collision with walls or players.

  7. BallDestroySystem: Queries for transform and velocity components to check if a ball has crossed the boundary line. If a ball entity crosses the boundary, it is destroyed, and points are added accordingly via the UIManager class.

Other

  1. Game Scenes The game consists of two main scenes: a menu scene and a game scene. The menu scene contains buttons for starting the game and other functionalities. The game scene includes the core game elements such as the middle line, points text, player spawner, and ball spawner entities. The spawner entities include only the transform component and are not visually represented.

  2. Prefabs Prefabs used in the game include a ball prefab and a player prefab. The ball prefab, for example, includes a transform component, a sprite renderer for visualization, and an authoring script that adds a velocity component during entity creation.

  3. Monobehaviour scripts

    • GameManager: Handles points management, game end conditions, and determining the winner. It stops the execution of other systems and displays the game result when a game ends.
    • PongMenuHandler: Manages the behavior of buttons in the menu scene, such as starting the game and creating server and client worlds when the "Host Game" button is pressed.

Package Lockstep Netcode Model Description and Usage

Package implements a basic deterministic lockstep netcode model with added forced input latency. This netcode model is used by the sample game and may be used in custom game implementation. The package is not using any netcode packages provided by Unity like for example Netcode for Entities and instead all of netcode necessary elements are created within this package. The most important parts of it are:

Client and Server Modes

In order to control the behaviour of server and client, approperiate singleton component exists (DeterministicServer and DeterministicClient) which contains an enum named DeterministicClientWorkingMode which represents the current working mode of the client or server. This state can be changed manually by developer. This component is created automatically by the package.

Server Modes

For the server within server world the following modes are available:

Client Modes

For the client within client world the following modes are available:

Determinism Validation and Debugging Tooling

The determinism validation is performed by hashing the state of the game and comparing the hashes between clients on each tick. Because hashing entire state of the game would be to costly performance wise the approach of marking specific components and entities for validation is used. In order to include a component in the validation process, the developer needs to add the desired component types to the DeterministicComponentList, which uses a DynamicTypeList to store these types. This singleton component is automatically created and includes essential components like for example LocalTransform or GameSettings. Developers can query and add additional component types to this list at the beginning of the game. For entity validation, the CountEntityForWhitelistedDeterminismValidation component should be added to the prefabs of entities to be validated (present in DeterministicEntityAuthoring). This setup allows for either FullStateValidation or WhitelistedStateValidation, providing flexibility for both debugging and production use.

The game includes a determinism validation system with several validation options:

  1. None: No validation is being performed.
  2. WhitelistHashPerTick: Hash of marked components only on marked entities per each tick is being computed.
  3. FullStateHashPerTick: Hash of all marked components per each tick is being computed.
  4. WhitelistHashPerSystem: Hash of marked components only on marked entities per each system in the tick is being computed.
  5. FullStateHashPerSystem: Hash of all marked components per each system in the tick is being computed.

In order to guarantee the deterministic order of hashing and later debugging files for now an approach is to add an DeterministicEntityID component to entities that are considered for validation, representing a unique, deterministic identifier (incremented each time). This approach has a drawback that developers must remember to add this component to each entity they create (e.g., every time a ball is spawned) and increment the counter deterministically. Incorrect sorting due to oversight can result in incorrect final hash values. Only entities with this component are considered for validation, making it crucial for developers to ensure this ID is added to any entity they want to include in validation. The current drawback of needing to ensure that DeterministicEntityID is added to an entity will be addressed in a future iteration by creating code that automatically adds and increments its value for every entity created or added to the scene. This would result in the need for only a deterministic order of entity creation, which, while still challenging, would be an improvement.

The validation system hashes game state components and sends hashes to the server for validation. If desync is detected, RPC to signal this event is send to clients to stop game execution and log files are generated for debugging.

Log File Generation

When desync occurs, log files are generated to help identify the source of nondeterminism:

Replay functionality

It's possible to replay the game in order to use different validation method to obtain informations about a particular desync. In order to use it GameSettings and ServerInputRecording files should be placed in main NondeterminismLogs folder and isReplayFromFile variable in DeterministicSettings should be set to true. It allows for local simulation based on save inputs and settings. In this mode the game is played locally and because of that the generated file will contain informations per each tick (not only the last one). The advantage of this approach is that the developer can choose a different validation method for the replay (e.g., per-system validation) and examine the logs of the simulation again. Since this is a local resimulation, it will only create log files on one device. Therefore, it is important to use a machine with the same specifications as the one where nondeterminism was detected, which can be specified based on the ClientSettings file. Nondeterminism issues may be stable, appearing on the same tick every time (e.g., an issue related to spawning the last ball on tick 1000 in the sample Pong game), or they may be unstable, appearing on seemingly random ticks. To address this, the replay file will not only hash the final state but also generate a file with hash information for every tick in the game. This allows for detecting the first variable that diverged between two runs.

Package Usage With Other Game

In order to use deterministic lockstep netcode model implemented by this package you can follow the implementation of the sample Pong game. The most important aspects are that in order for it to work, several steps need to be done.

The package will be a subject for modifications and improvements and this README may change or be updated.

Future Improvement Plans

The following elements are considered as parts of "future work" and will be implemented in the future:

Netcode Model Enhancements

Determinism Validation and Debugging Tool Improvements

Contributing

Any contributors are welcome to help improve this package. To contribute:

  1. Fork the Repository: Create a personal fork of the repository on GitHub.
  2. Clone the Fork: Clone your fork to your local machine.
  3. Create a Branch: Create a new branch for your feature or bug fix.
  4. Make Changes: Implement your changes in the new branch.
  5. Commit and Push: Commit your changes and push the branch to your fork on GitHub.
  6. Create a Pull Request: Open a pull request from your fork's branch to the main repository's main branch.

License

The package is available under The MIT License. This means it is free for commercial and non-commercial use. Attribution is not required, but appreciated.