Lance Multiplayer Game Server

repository·master·Indexed 23 days ago

https://github.com/lance-gg/lance

A Node.js-based real-time multiplayer game server framework (version 5.0.2) that provides networking infrastructure, state synchronization, and lag compensation. It features optimized binary communication via WebSockets, interpolation and extrapolation for smooth motion, and support for multiple physics engines including P2 for 2D and Cannon for 3D. The framework separates the ServerEngine (infrastructure), GameEngine (logic), and ClientEngine (synchronization and input), allowing developers to focus on game logic while the framework handles netcode.

Tokens
16.1K
Snippets
23
Records
95
Agent score
82%

What's inside Lance

  1. Overview of Lance features and capabilities

    master

    Lance is a real-time multiplayer game server built on Node.js that provides an extendible server for game logic and a client-side library for state synchronization. It is designed to optimize the player's visual experience through several core networking and synchronization features:

    • Optimized Networking: Uses TCP via WebSockets with packed and serialized binary communication. It includes automatic handling of network spikes via step correction.
    • Lag Handling & Synchronization: Supports Extrapolation (client-side prediction with step re-enactment) and Interpolation for smooth object motion.
    • Game State Management: Handles user input coordination, shadow objects, physics, and pseudo-physical movement.
    • Developer Experience: Provides tools for debugging and tracing to simplify the development of complex multiplayer logic.
  2. Overview of Lance real-time multiplayer game server

    master

    Lance is an extendible Node.JS-based game server designed to handle netcode, allowing developers to focus on game logic. It includes a client-side library that synchronizes the client's game state with the server.

    Key capabilities include:

    • Optimized Networking: Uses TCP via WebSockets with packed and serialized binary communication. It includes automatic handling of network spikes via step correction.
    • Lag Handling: Implements intelligent sync strategies such as Extrapolation (client-side prediction with step re-enactment) and Interpolation for smooth object motion.
    • Visual Smoothing: Provides position interpolation/extrapolation, user input coordination, shadow objects, and physics/pseudo-physical movement.
    • Developer Experience: Provides tools for debugging and tracing.
  3. What is Lance?

    master
    Lance is an open-source real-time multiplayer game server designed for writing physics-networked games in JavaScript. It provides built-in implementations for networking, client-side prediction of object positions, extrapolation, and 2D/3D networked physics, allowing developers to focus on game logic rather than low-level networking synchronization.
  4. How the ServerEngine executes game steps

    master

    The ServerEngine manages the authoritative game state by executing a loop at a fixed interval. The sequence of operations during a single server step is:

    1. ServerEngine (Step Start): Initiates the server step.
    2. GameEngine (Input Processing): Calls GameEngine::processInput() to read and process all inputs received from clients since the last step.
    3. GameEngine (Game Step): Executes the game logic, which includes calling the PhysicsEngine to handle physics calculations.
    4. Broadcast (Sync): If the configured synchronization interval is met, the server transmits a "world update" to every connected player.
  5. Debug Client Extrapolation

    master

    In extrapolation mode, the client re-enacts the required history of steps every time a new sync is received. To debug issues related to 'bending' (corrections), compare these three states in your traces:

    1. The position of the object just before the sync was received.
    2. The result of the re-enactment before bending was applied.
    3. The final object position after bending was applied.
  6. Define a netscheme for networked attributes

    master

    To synchronize custom data, a game object sub-class must define a netscheme. The netscheme is a dictionary of networked attributes. Only the attributes listed in the netscheme will be serialized by the server and broadcast to all clients during every sync.

    Note that the DynamicObject base class only implements positional attributes by default. Any additional attributes—such as power, energy, or health—must be explicitly specified in the netscheme to be synchronized.

  7. Core components of a Lance game

    master

    A Lance game is composed of several key architectural components that you will typically extend or configure:

    • ClientEngine: Represents a client. Multiple instances exist (one per player). They collect player inputs and send them to the server.
    • ServerEngine: A singleton instance that handles user inputs and broadcasts updates to all clients.
    • GameEngine: The core class where you implement your game's specific logic. You must create a subclass of GameEngine to define your game.
    • GameObject: The base class for all entities within the game world.
    • Renderer: A component responsible for drawing game visuals during the render loop.
    • Synchronization: A configurable mechanism used to align the state between the server and clients.

    To build a game, you will primarily implement your core logic by extending the GameEngine class.

  8. Define synchronized Game Objects with DynamicObject

    master

    Game objects should extend the DynamicObject class. To synchronize data between the server and clients, define a netScheme static getter. This scheme specifies which attributes are sent over the network and their types using BaseTypes.

    To ensure the client's state matches the server's, implement the syncTo(other) method, which copies attributes from the server's object to the client's object.

    Example of a synchronized Paddle object:

    class Paddle extends DynamicObject {
        constructor(gameEngine, options, props) {
            super(gameEngine, options, props);
        }
    
        static get netScheme() {
            return Object.assign({
                health: { type: BaseTypes.TYPES.INT16 }
            }, super.netScheme);
        }
    
        syncTo(other) {
            super.syncTo(other);
            this.health = other.health;
        }
    }
    class Paddle extends DynamicObject {
    
        constructor(gameEngine, options, props) {
            super(gameEngine, options, props);
        }
    
        static get netScheme() {
            return Object.assign({
                health: { type: BaseTypes.TYPES.INT16 }
            }, super.netScheme);
        }
    
        syncTo(other) {
            super.syncTo(other);
            this.health = other.health;
        }
    }
  9. How extrapolation synchronization works

    master

    Extrapolation is a synchronization method where the client predicts the game's future progress in the absence of server data. The client renders steps ahead of the server (e.g., the server is at step 1026 while the client renders step 1030).

    When sync data arrives from the server, the client must reconcile its predicted state with the actual server state using a process called reconciliation:

    1. Roll back: The client reverts its state to the step described by the server (step M).
    2. Re-enact: The client re-runs all steps from M up to the current client step (N) by calling the game engine's step() method. These are called re-enactment steps.
    3. Bending: Because re-enactment results in different positions than the original prediction, the client applies a delta (the difference between predicted and actual positions). Instead of snapping to the correct position, the client uses a bending factor to gradually move objects toward the correct position over multiple steps to avoid visual jitter.

    Key Implementation Concepts

    • Re-enactment: The game engine's step() method must be able to handle being called multiple times for the same step. The step() method receives an argument indicating whether the current call is a re-enactment step.
    • Bending: Bending applies to positions and velocities. Designers should avoid bending for objects that teleport or have sudden impulse-based velocity changes.
    • Shadow Objects: When a client performs an action that creates an object (like firing a missile) before the server has acknowledged it, the client renders a shadow object to represent that entity until the official server object is created.
  10. How Interpolation synchronization works

    master

    Interpolation is a synchronization method where clients render game steps that have already been processed by the server. Instead of trying to predict the future, the client renders a slightly delayed version of the past.

    How it works

    • The server advances through game steps (e.g., step 1026).
    • The clients render older steps (e.g., step 1010).
    • Because the client has access to both the current state and the state from a few steps ago, it can interpolate (smoothly transition) object positions between those two points, resulting in visually smooth motion.

    Trade-offs

    • Advantage: Provides very smooth visual motion because the client always has the 'future' data (relative to its current render step) to interpolate towards.
    • Disadvantage: Introduces input lag. A player's input will not show a visual consequence until the client's render step catches up to the server step where that input was processed.

    Implementation Considerations

    • Tunable Delay: The time delay between the server and clients is a tunable parameter. You should choose the smallest delay possible that still allows for enough sync data to perform interpolation. If network spikes occur, the client may stop rendering while waiting for data; the client should be designed to reduce the delay gradually to recover from these spikes.
    • Atomic Actions: Not all game logic can be interpolated. Actions like shooting or bouncing off walls are "atomic" and must be explicitly marked as such to prevent visual nonsense. Interpolation should only be used for continuous state like position or rotation.
    • Client Requirements: The game engine does not necessarily need to run on the client if the server sync provides all the information required for rendering.
  11. Implement a GameEngine sub-class

    master

    The GameEngine is the core of your game, running on both the server and the client. The server's execution is authoritative, while the client runs the engine to predict movement.

    Registration

    In your GameEngine sub-class, use registerClasses(serializer) to register all your DynamicObject types so the Lance serializer can handle them.

    Event Handling

    Use this.on(eventName, handler) in the constructor to respond to game lifecycle events. Common events include:

    • postStep: Runs after every game step (ideal for game logic).
    • server__init: Server-only initialization.
    • server__playerJoined: Triggered when a new client connects.
    • server__playerDisconnected: Triggered when a client leaves.
    • client__rendererReady: Client-side initialization.
    • client__draw: Client-side rendering loop.
    class Game extends GameEngine {
        constructor(options) {
            super(options);
            this.physicsEngine = new SimplePhysicsEngine({ gameEngine: this });
    
            // common code
            this.on('postStep', this.gameLogic.bind(this));
    
            // server-only code
            this.on('server__init', this.serverSideInit.bind(this));
            this.on('server__playerJoined', this.serverSidePlayerJoined.bind(this));
            this.on('server__playerDisconnected', this.serverSidePlayerDisconnected.bind(this));
    
            // client-only code
            this.on('client__rendererReady', this.clientSideInit.bind(this));
            this.on('client__draw', this.clientSideDraw.bind(this));
        }
    
        registerClasses(serializer) {
            serializer.registerClass(Paddle);
            serializer.registerClass(Ball);
        }
    }
    constructor(options) {
        super(options);
        this.physicsEngine = new SimplePhysicsEngine({ gameEngine: this });
    
        // common code
        this.on('postStep', this.gameLogic.bind(this));
    
        // server-only code
        this.on('server__init', this.serverSideInit.bind(this));
        this.on('server__playerJoined', this.serverSidePlayerJoined.bind(this));
        this.on('server__playerDisconnected', this.serverSidePlayerDisconnected.bind(this));
    
        // client-only code
        this.on('client__rendererReady', this.clientSideInit.bind(this));
        this.on('client__draw', this.clientSideDraw.bind(this));
    }
    
    registerClasses(serializer) {
        serializer.registerClass(Paddle);
        serializer.registerClass(Ball);
    }
  12. How the ClientEngine executes game steps and rendering

    master

    The ClientEngine manages the local player experience and must reconcile its state with the server. It operates in two distinct loops:

    Client Game Step

    Occurs at a fixed interval to process local logic:

    1. Check Syncs: Inspect inbound messages/syncs from the server to reconcile the local state.
    2. Capture Inputs: Collect user inputs since the last step.
    3. Send Inputs: Call ClientEngine::sendInput() to transmit captured inputs to the server.
    4. Apply Inputs: Apply those inputs locally to the client's version of the game state.

    Client Render Step

    Occurs as frequently as the hardware allows to draw the game:

    1. Renderer (Draw Event): Triggers the drawing process.
    2. GameEngine (Game Step): The GameEngine may execute zero or more steps during a single render event to catch up to the current time (e.g., if multiple physics steps are required between frames). This includes running the PhysicsEngine step.