rameshvarun / netplayjs

Make P2P multiplayer browser games, no server hosting or synchronization code required. Powered by rollback netcode + WebRTC.
https://rameshvarun.github.io/netplayjs/
ISC License
495 stars 34 forks source link
game-development game-networking gamedev multiplayer multiplayer-browser-game multiplayer-game p2p rollback-netcode threejs typescript webrtc

NetplayJS

Node.js CI npm

Make peer-to-peer WebRTC-based multiplayer games in JavaScript, no server hosting or network synchronization code required!

[CHECK OUT THE DEMOS]

Quick Start

Remix on Glitch

Here's how NetplayJS works:

NetplayJS handles most of the complicated aspects of multiplayer game development, letting you create games almost as if they were local multiplayer games. Synchronization and matchmaking are handled automatically under the hood - and best of all you don't have to host any servers!

Let's make a very simple game. Create an HTML file and add the following script tag.

<script src="https://unpkg.com/netplayjs@0.4.1/dist/netplay.js" integrity="sha384-6Yb8LWAT488jwK+nIjvD4S5/poq1Xn69NYjH1RXKHoaUOaFJrKQ1rfGQgKm8oQjX" crossorigin="anonymous"></script>

Now add this javascript code to the same HTML somewhere within the <body>.

<script>
class SimpleGame extends netplayjs.Game {
  // In the constructor, we initialize the state of our game.
  constructor() {
    super();
    // Initialize our player positions.
    this.aPos = { x: 100, y: 150 };
    this.bPos = { x: 500, y: 150 };
  }

  // The tick function takes a map of Player -> Input and
  // simulates the game forward. Think of it like making
  // a local multiplayer game with multiple controllers.
  tick(playerInputs) {
    for (const [player, input] of playerInputs.entries()) {
      // Generate player velocity from input keys.
      const vel = input.arrowKeys();

      // Apply the velocity to the appropriate player.
      if (player.getID() == 0) {
        this.aPos.x += vel.x * 5;
        this.aPos.y -= vel.y * 5;
      } else if (player.getID() == 1) {
        this.bPos.x += vel.x * 5;
        this.bPos.y -= vel.y * 5;
      }
    }
  }

  // Normally, we have to implement a serialize / deserialize function
  // for our state. However, there is an autoserializer that can handle
  // simple states for us. We don't need to do anything here!
  // serialize() {}
  // deserialize(value) {}

  // Draw the state of our game onto a canvas.
  draw(canvas) {
    const ctx = canvas.getContext("2d");

    // Fill with black.
    ctx.fillStyle = "black";
    ctx.fillRect(0, 0, canvas.width, canvas.height);

    // Draw squares for the players.
    ctx.fillStyle = "red";
    ctx.fillRect(this.aPos.x - 5, this.aPos.y - 5, 10, 10);
    ctx.fillStyle = "blue";
    ctx.fillRect(this.bPos.x - 5, this.bPos.y - 5, 10, 10);
  }
}

SimpleGame.timestep = 1000 / 60; // Our game runs at 60 FPS
SimpleGame.canvasSize = { width: 600, height: 300 };

// Because our game can be easily rewound, we will use Rollback netcode
// If your game cannot be rewound, you should use LockstepWrapper instead.
new netplayjs.RollbackWrapper(SimpleGame).start();
</script>

And voila - we've made a real-time networked game with rollback netcode and client-side prediction.

Overview

NetplayJS is a framework designed to make the process of creating multiplayer browser games simple and fun. It consists of several different components.

Installation

For simple usage, you can include NetplayJS directly from a script tag in an HTML file.

<script src="https://unpkg.com/netplayjs@0.4.1/dist/netplay.js" integrity="sha384-6Yb8LWAT488jwK+nIjvD4S5/poq1Xn69NYjH1RXKHoaUOaFJrKQ1rfGQgKm8oQjX" crossorigin="anonymous"></script>

For larger projects, you should install NetplayJS from npm and bundle it with your application using Webpack or a similar module bundler.

npm install --save netplayjs

I also highly recommend that you use it with TypeScript, though this is not required. The examples following will be in TypeScript.

Usage

To create a game using NetplayJS, you create a new class that extends netplayjs.Game.

class MyGame extends netplayjs.Game {
  // NetplayJS games use a fixed timestep.
  static timestep = 1000 / 60;

  // NetplayJS games use a fixed canvas size.
  static canvasSize = { width: 600, height: 300 };

  // Initialize the game state.
  constructor(canvas: HTMLCanvasElement, players: Array<NetplayPlayer>) {}

  // Tick the game state forward given the inputs for each player.
  tick(playerInputs: Map<NetplayPlayer, DefaultInput>): void {}

  // Draw the current state of the game to a canvas.
  draw(canvas: HTMLCanvasElement) {}

  // Serialize the state of a game to JSON-compatible value.
  serialize(): JsonValue {}

  // Load the state of a game from a serialized JSON value.
  deserialize(value: JsonValue) {}
}

You can now start the game by passing your game class to one of several wrappers.

Game State Serialization

The client-side prediction and rewind capabilities of netplayjs are based off of the ability to serialize and deserialize the state of the game. In the quickstart example above, we let the autoserializer take care of this. For most games, however, you will need to implement your own logic. You can do this by overriding Game.serialize and Game.deserialize in your subclass.

If you cannot serialize the game state, you can still use NetplayJS, but you will need to use Lockstep netcode, rather than predictive netcodes like Rollback, and you need to mark your game as deterministic.

NetplayPlayer

A NetplayPlayer represents one player in a game. NetplayPlayer.getID() returns an ID that is stable across each network replication of the game.

DefaultInput

NetplayJS games are synchronized by sending inputs across a network. DefaultInput automatically captures and replicates keyboard events, mouse events, and touch events.

FAQ

Does NetplayJS require game code to be deterministic?

NetplayJS does not require game code to be deterministic, but is more efficient if it is. By default, NetplayJS corrects for drift by having one player (the host) send authoritative state updates to the others. NetplayJS will skip these updates if you explicitly mark your game as being deterministic.

Whether or not JavaScript operations are cross-platform deterministic is a difficult question. Here's what I know:

Can NetplayJS be used with Unity, Godot, PlayCanvas, etc?

NetplayJS works best with lightweight game frameworks. The reason is we need game state to be kept in one place, so that it's easy to replicate across the network

Other engines tend to have complicated entity systems with their own state management, and wont fit nicely into the NetplayJS state model.

One way to get around this is to essentially create an invisible NetplayJS game that runs in the background and describes the actual game logic. Then, on draw(), instead of drawing directly, use the current game state to update the entities in your game engine's scene.

Assets Used from Other Projects

This repo contains code and assets from other open source projects.