Skip to content

Instantly share code, notes, and snippets.

@PsySecCorp
Created January 15, 2025 00:35
Show Gist options
  • Save PsySecCorp/a03e77395f06f3497b8b3bd7b88a2cae to your computer and use it in GitHub Desktop.
Save PsySecCorp/a03e77395f06f3497b8b3bd7b88a2cae to your computer and use it in GitHub Desktop.
worldEngine.md

https://docs.phaser.io/api-documentation/class/physics-arcade-world

You're absolutely right! In a multiplayer setting, directly adjusting objects on the screen can lead to inconsistencies due to network latency, differing client states, and other issues. To resolve this, using a physics space (often referred to as a "simulation" or "world" state) to handle the authoritative logic and movement makes things much more predictable and synchronized. Here’s a breakdown of how you can structure this:

1. Separation of Logic and Rendering

  • Physics Space (Simulation): This is the authoritative "truth" of the game. All changes to object positions, velocities, and other states (health, status, etc.) happen here. Think of it as a snapshot of the game state that updates according to both local and network inputs.
  • Rendering Layer: The visual representation of the objects is based on interpolating between past and current physics states to smooth out any discrepancies caused by lag or packet loss.

2. Handling Inputs and Commands

  • Player Input: On the client-side, user inputs (mouse/keyboard) trigger changes in the local simulation state. However, those changes are then sent to the server in a network message (or event), which applies them to the authoritative physics space.
  • Server Logic: The server processes the inputs from all clients, updates the authoritative physics space, and then broadcasts the updated state to the clients.
  • Client Prediction: The client can locally predict and simulate movement based on input before receiving the server's confirmation, which allows for a more responsive feel. When the server state arrives, the client can correct the position based on interpolation.

3. Physics Space Structure

  • Entities: Define entities in terms of their states (e.g., position, velocity, acceleration, etc.).
  • Event Processing: Both game events (e.g., collision detection, item pickups) and network events (e.g., position updates from other players) should be processed in this physics space.
  • Time Steps: You should run the physics simulation on fixed time steps (for consistency), for example using a "tick" system. This ensures that all clients and the server are in sync about the physics world state.

4. Interpolation for Smoothing

Since physics and network updates happen at different intervals (and typically network updates may have a delay), you can implement interpolation to smooth the visuals. Here’s how you can do that:

  • Interpolation Between States: On the client, interpolate between the last known position and the updated position of entities using the time difference between the last received update and the current frame.
  • History Buffer: Maintain a small buffer of the last few physics states so that you can interpolate smoothly between positions when updates are received.

5. Lag Compensation and Prediction

  • Lag Compensation: When the client receives a network update (e.g., player position from the server), it can compensate for the lag by adjusting positions and velocities based on the lag time.
  • State Reconciliation: If the client’s predicted state differs too much from the server’s state (e.g., after a lag spike), you can reconcile the state by correcting the client’s position, often smoothly, so the game doesn’t feel jarring to the player.

6. Practical Steps in PhaserJS

Here’s a rough outline for implementing this architecture in PhaserJS:

  • Create a Physics World:

    var world = new Phaser.Physics.Arcade.World(scene);
  • Handle Network Updates: When you receive network data, you would update the physics world with the authoritative data. For example:

    function updatePhysicsFromServer(serverData) {
        serverData.entities.forEach(entity => {
            // Update positions, velocities, etc.
            let phaserEntity = world.getEntity(entity.id);
            phaserEntity.setPosition(entity.x, entity.y);
            phaserEntity.setVelocity(entity.velocityX, entity.velocityY);
        });
    }
  • Handle Local Player Input: On the client, when the player inputs something (e.g., moving), you can predict the movement locally:

    // Local player movement prediction
    if (input.isMoving()) {
        localPlayer.velocity.x = input.direction.x * speed;
        localPlayer.velocity.y = input.direction.y * speed;
    }

    After applying local prediction, send this data to the server for confirmation.

  • Render with Interpolation: To make rendering smooth even when updates arrive at different times:

    function renderInterpolatedPositions() {
        // Interpolate positions based on last and current physics state
        let alpha = (gameTime - lastUpdateTime) / (currentUpdateTime - lastUpdateTime);
        localPlayer.x = Phaser.Math.Interpolation.Linear([lastPosition.x, currentPosition.x], alpha);
        localPlayer.y = Phaser.Math.Interpolation.Linear([lastPosition.y, currentPosition.y], alpha);
    }

7. Considerations for Multiplayer

  • Client-Side Prediction: Always predict the results of user input locally so the game feels responsive, and then correct the prediction based on authoritative updates from the server.
  • Server-Side Authority: Always trust the server as the final source of truth for entity positions, health, and other critical game states.
  • Bandwidth: Minimize the amount of network traffic by sending only the necessary updates, and compress the data where possible.

Summary

  1. Create a physics space as the authoritative model of your game state.
  2. Interpolate between past and current states to smooth rendering.
  3. Handle local player inputs with prediction and server updates with reconciliation to keep everything in sync.
  4. Use network events and game events to update the physics space and inform clients of the state.

This approach will help you manage multiplayer game states in a way that feels smooth and responsive while avoiding inconsistencies due to network latency.

@HyperCrowd
Copy link

To include the ID of each body in the state array, you can make use of the body.id property (if your bodies have unique IDs assigned) or create a custom ID property if necessary. In Phaser 3, physics bodies typically don't have an explicit id by default, but you can assign IDs manually or use their unique reference within the world.

Here’s an updated version of the getPhysicsStateAsArray function, which includes the ID of each body:

Updated Example Code:

function getPhysicsStateAsArray() {
    let stateArray = [];

    // Loop through all active bodies in the physics world
    world.bodies.forEach(body => {
        if (body.active) {
            // Include body ID, position, velocity, rotation, and angular velocity
            stateArray.push(
                body.id,                              // Body ID (if assigned)
                body.x, body.y,                        // Position (x, y)
                body.velocity.x, body.velocity.y,      // Velocity (x, y)
                body.rotation,                         // Rotation
                body.angularVelocity                   // Angular velocity
            );
        }
    });

    return stateArray;
}

Explanation:

body.id: The id of each body. If you haven't manually assigned an id to each body, you can use the body reference itself as a unique identifier. If you'd like to add a custom id, you can set it when creating the body (e.g., body.id = someUniqueID).
The rest of the properties are as before: position (x, y), velocity (velocity.x, velocity.y), rotation (rotation), and angular velocity (angularVelocity).

Example with Manual ID Assignment:

If you haven't assigned IDs to the bodies yet, you can manually set them when creating or adding the bodies, like so:

let body = this.physics.world.enable(sprite);
body.id = uniqueID;  // Assign a custom ID

Now, when you call getPhysicsStateAsArray(), it will include these IDs in the returned array.

Example Output:
If you have multiple bodies in the physics world, the output might look like this (simplified example):

[
    1, 100, 150, 2, 3, 0.5, 0.1,   // Body 1: ID, position (100,150), velocity (2,3), rotation (0.5), angularVelocity (0.1)
    2, 200, 250, 4, 5, 0.3, 0.05,  // Body 2: ID, position (200,250), velocity (4,5), rotation (0.3), angularVelocity (0.05)
    // More bodies...
]

Custom ID Approach (if needed):

If you don't use body.id, you can use the index of the body in the world as a unique identifier (though this is less flexible):

stateArray.push(
    body.index,                           // Using index as a unique ID
    body.x, body.y,
    body.velocity.x, body.velocity.y,
    body.rotation,
    body.angularVelocity
);

Let me know if you need further clarifications or adjustments!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment