I’ve been creating web games for a while now. I remember doing my first ones around 2017, following YouTube tutorials by Daniel Shiffman, which introduced me to p5.js a JavaScript library that makes it easy to create graphics and interactive content in the browser.

My old p5.js projects

Projects were simple but fun, like this Langton’s Ant simulation:

Getting Into Multiplayer Games

I mostly play multiplayer games. Adding multiplayer to even a simple game opens up endless possibilities, so I started learning about networking, multiplayer architectures, hosting backends, and so on.

When you Google this stuff, you find premade solutions within engines like Fish-Net for Unity, or third-party services like Photon. They’re great, but I wanted to learn how to make my own multiplayer games from scratch (relatively speaking, of course—we’re still in the JavaScript world!).

TCP or UDP?

One of the first things you learn when making websites are REST APIs (HTTP). The client sends a request, the server responds with data (usually JSON). You could use this for multiplayer games, but it’s not ideal. HTTP is stateless, so every little action needs a new request-response cycle—bad for performance and latency, and not real-time.

Older web games used HTTP long polling: the client sends a request, the server holds it until there’s new data, sends the response, and the client immediately sends another request. This was heavy on latency and performance, but it was necessary because Internet Explorer didn’t support WebSockets back then.

old IE versions

Libraries like Socket.io (by the same creator as Vercel) abstracted the transport layer, using WebSockets if available and falling back to long polling otherwise.

WebSockets give you a persistent, real-time connection between client and server. They’re TCP-based, so packets are guaranteed to arrive in order.

That sounds good, but it’s not perfect for fast-paced multiplayer games. If a packet is lost, the following packets get delayed until the lost one is retransmitted and received—a problem called head-of-line blocking. Packet loss happens all the time in real networks, which can lead to high latency and jitter.

You can simulate packet drop with tools like “clumsy” on Windows. The TCP acknowledgment mechanism can be a real pain in fast-paced environments. (I didn’t knew this back then :D)

UDP is the standard for multiplayer games. It’s faster than TCP because it doesn’t have the overhead of reliability (no ACKs), but it isn’t natively supported in browsers. You’d have to use WebRTC DataChannels, which are more complex to set up. These days, libraries like Geckos make it easier to use UDP in the browser.

Think of TCP like a group chat where you read every message in order. If one text goes missing, everything stops until it shows up. In a fast game, you don’t care about the old “I’m here” from a second ago, you just need the latest “I’m here now!”. That’s where UDP shines

What Do Other Web Games Use?

  • Krunker.io → WebSockets Krunker.io
  • Slither.io → WebSockets Slither
  • Haxball → WebRTC (UDP) haxball

Nowadays, most browsers support WebRTC quite well

For simplicity, I decided to go with WebSockets.

My First Multiplayer Game: Pong

pong 2017. definitely a GOTY.

The game state was simple:

  • Paddle positions (y-coordinate)
  • Ball position (x, y)
  • Ball velocity (speed in x and y)

Since your client controls the left paddle, you can instantly update its Y position based on your mouse movement on the canvas without waiting for the server to confirm your input. This approach makes the paddle feel more responsive, even if it means your local paddle position might briefly differ from the server’s authoritative state.

I sent messages from client to server like this:

{
  "type": "move_paddle",
  "y": 150
}

The client’s position was trusted— hich is bad for security because a player could cheat by sending invalid input, but for Pong, a little Y-axis teleportation wasn’t a big deal.

The server held the authoritative game state with a simple clients array:

class Client {
  gameStarted: boolean; // used for matchmaking
  posX: number;
  posY: number;

  constructor(public id: string) {
    // socket id
    this.gameStarted = false;
    this.posX = 0;
    this.posY = 0;
  }
}

I built a basic matchmaking system: when players joined, they’d be paired up, and the server would create a new game instance for them.

back then I didn’t know how to make the game loop properly, so I used setInterval to update the game state at a fixed tick rate (e.g., 30 times per second). Each tick, the server would update the ball’s position based on its velocity, check for collisions with paddles and walls, and send the updated game state to both clients.

client rendered everything as it received updates from the server. The server sent messages like this:

{
  "type": "game_state",
  "ball": { "x": 200, "y": 150 },
  "paddles": [
    { "id": "player1", "y": 120 },
    { "id": "player2", "y": 180 }
  ]
}

this isn’t a well made approach because the server’s tickrate doesn’t match the client’s framerate, leading to choppy movement of the ball, we could at least interpolate it between 2 server updates, but I didn’t do that back then.

This was my first multiplayer game. It was simple, but it worked!

I hosted it on a free Heroku dyno. back then heroku had a generous free tier.

The dyno would sleep after 30 minutes of inactivity, so the first player to join had to wait for it to wake up. A bit annoying, but it was free.

Why I Ended Up Making My Own Roblox-like Engine

After Pong, I kept making other multiplayer games, like a top-down 2D shooter called “Akrely” that I never released (circa 2018?). They were fun, and I learned a ton about networking

But every time I wanted to start a new multiplayer project, I found myself reimplementing the same boilerplate: networking code, the game loop, rendering, serialization, event systems, input handling… you name it.

I started to know more about common netcode patterns, like client-side prediction, server reconciliation, and lag compensation. Implementing these from scratch for every new game was tedious.. it’s a little bit of a rabbit hole tbh.

I often play Source engine games, which are famous for their countless player-made mods and custom servers.

Then I saw roblox, which is basically a game engine with a built in multiplayer system, even tho I don’t really play that game, the idea of having a game engine with a built in multiplayer system was really appealing, imagine just putting a few lines of scripts (Lua in roblox’s case) and having a multiplayer game up and running?

I was sold. let’s do it!

But how? What architecture? How much would it cost? Wait I’m on the web too! I need this to work in JavaScript.. How much time would it take? Only one way to find out.

Building NotBlox

Why Not Use Existing Engines?

I don’t use Unity. Unreal or Godot.

I want to do it from ““scratch”” (using existing libraries of course, but not a full game engine) -> fast first load time, native web experience, no unity splash screen, no godot export size, no unreal engine (i’m mostly on linux too so.. it’s easier this way)

Basically those engine provides a webgl export with emscripten, which produces wasm + js glue code that’s usually pretty big (multiple MBs) and has a lot of overhead. but it works great for many use cases. Unity webgl games are pretty common.

3D Rendering in the Browser

By then, I had some experience with 3D rendering using Three.js. I’d already worked on projects like a TypeScript FPS engine (enari-engine) with client-side 3D and collisions.

No need to reinvent the wheel. Three.js is a solid 3D library with tons of community resources, I knew I could render things on the screen without much hassle thanks to their tutorials. Also I knew I could run three.js headlessly, which is gonna be useful

3D Collisions

For 3D physics, I’d seen impressive browser demos like Sketchbook, which used cannon.js. I’d also tried ammo.js (a port of Bullet physics via Emscripten), but cannon.js was much simpler and lighter. My old repo used ammo.js, but the bundle size was over 2MB. Cannon.js was around 500KB.

there is also a TypeScript fork called cannon-es

The Prototype

I started with separate repos for frontend and backend. The initial prototype used a simple OOP approach: a base class for game objects, with subclasses for players, NPCs, items, etc.

Well… it didn’t go well. I stopped.

The OOP approach introduced inheritance and coupling that became hard to manage. While browsing gamedev forums, I discovered ECS (Entity Component System) architecture, which promised more flexibility and better separation of concerns.

We’re taught to default to OOP it feels natural. But in game dev, ECS is often preferred because it separates data (components) from behavior (systems), making complex game states and interactions much easier to manage.

So I dove into ECS and rebuilt everything from scratch.

It was a lot of work, but I learned a huge amount. After a few months, I had a basic multiplayer game engine running with ECS architecture, a networking system, and a game loop.

I called it NotBlox—because it’s not Roblox, but it’s inspired by it.

You can check it out here: notblox.online