Building a Multiplayer Game Engine With Deno and TypeScript

How (and why) we built Dreamlab: the multiplayer-first game engine
Published Thursday, September 19, 2024Posted by Jackson Roberts

Dreamlab editor running in the browser. 👉 Star us on GitHub!

The following three statements are true:

  1. Games are more fun with friends.
  2. Web games requiring no download are the most accessible.
  3. Almost every techie has dreamt of making games.

Since you clicked on this blog post, I would bet that you got into programming from a desire to make or mod video games!

While the barrier of entry for game creation has gotten much lower, multiplayer solutions are extremely fragmented and web export support remains limited*. Making multiplayer games is still hard in 2024; hosting servers is a pain and tools like ChatGPT struggle to write code for multiplayer games due to inconsistent networking APIs.

This is why we're building Dreamlab, a new open-source multiplayer game engine. Our mission it to make multiplayer game development accessible to everyone. We are creating the open-source multiplayer game engine. Networking is a first-class abstraction and games can be scripted using elegant TypeScript.

Why Deno?

The original version of Dreamlab used node.js and multiple npm packages. The biggest problem we faced with this was dependency management. During development, we had to use npm link, keep build processes constantly running, and even manually overwrite our libraries in node_modules to get things working. This sucked!

In case you're unfamiliar, npm link works by replacing your package.json entries with a local file reference:

// npm package.json

// Before npm link: a specific reference
"@dreamlab.gg/core": "0.0.84-dev"

// After npm link: a file reference
"@dreamlab.gg/core": "file:../dreamlab-core"
// This cannot be commited as it will break your ability to build the package in isolation.
// WE ALWAYS HAD TO REVERT THIS BEFORE COMMITING.

We solved this problem by switching to a Deno monorepo. Now, instead of having to switch between a tagged version and a local package, we can always reference the local copy:

// deno.json
"imports": {
  "@dreamlab/engine": "../engine/mod.ts",
}

You may be thinking "this could have been accomplished using an npm monorepo setup, this functionality is not exclusive to Deno", and you'd be correct. However, by switching to Deno and monorepos simultaneously we were able to also eliminate the need to keep build processes running for every entrypoint. When working with TypeScript in node.js, you need to transpile your code before it can be used on the client or the server. Deno does not have this requirement and can run TypeScript directly.

Another major advantage of Deno is the ability to run untrusted code. Dreamlab's multiplayer server is capable of running multiple game servers at once and we operate a public hosting service where anyone can deploy to our infrastructure. With our old node runtime, we had to be extremely careful with user permissions when spawning processes that ran user code. However, Deno's security model allows us to deny filesystem, environment, and network access by default! The game scripts can only access system resources through the APIs that are passed into its environment.

The Editor

Dreamlab's editor is modeled after the familiar interfaces of Unity and Godot. On the left-hand side, we have the scene hierarchy and below that the file tree. On the right side, we have the inspector which shows the currently selected entity and its attached behaviors.

Screenshot of the Dreamlab game engine editor UI

From a technical perspective, the most interesting thing is that this does not use React or any UI libraries that manage their own state. In early versions of our engine, we did use React. However, synchronizing React's state with the game engine's actual values was very complex and error prone. To fix this, we switched to using web component custom elements and other vanilla DOM libraries for rendering the UI. Now, the UI always displays the correct values with a single source of truth.

Entity System

Entities are the fundamental building blocks of games in Dreamlab. They are equivalent to Godot's nodes or Unity's GameObjects.

Screenshot of the Dreamlab game engine editor UI showing the available entities

Entities provide functionality such as colliders, sprite renderers, clickable areas, etc. They aren't very interesting on their own. They only do something when a Behavior is attached to them.

Behavior System

Functionality in Dreamlab is implemented via Behavior classes. These are similar to MonoBehaviors in Unity. Behaviors are attached to entities to make them do things!

Below is the simplest possible example of a Behavior. It makes whatever entity it's attached to spin:

import { Behavior } from '@dreamlab/engine'

export default class SpinBehavior extends Behavior {
  speed: number = 1

  onInitialize() {
    // Make the "speed" value visible in the editor and networked between clients
    this.defineValue(SpinBehavior, 'speed')
  }

  onTick() {
    // Only run on the server
    if (!this.game.isServer()) return
    // Rotate the entity!
    this.entity.transform.rotation += this.speed
  }
}

When we attach this behavior to an entity by simply dragging it on and start the game, it spins:

We use esbuild to build these Behavior scripts for running on the client and can run then unmodified on the server. We automatically build every file that exports a Behavior so it can loaded and added in the editor.

Scene Graph and Networking

Every entity in Dreamlab exists under one of four roots:

  • world
    • Entities here are synced between all clients. Used for the game world itself that all players share.
  • server
    • Entities exist only on the server. Useful for managers, etc.
  • local
    • Entities exist only locally. Good for HUD UIs, particle effects, or other things that are client-only.
  • prefabs
    • Entities that exist to have copies of them instantiated.

Under the world root, entity transforms (position, rotation, scale) automatically sync between all clients. Behavior class properties that are registered using the defineValue API sync between all clients and are visible in the inspector. This allows for a particularly magical programming experience because you effectively have networked class instances where every property is automatically synced between all clients.

Why you should use Dreamlab

Dreamlab is for anyone who wants to create 2D multiplayer games! We want to create a world where everyone can create games and play them with their friends; Dreamlab aims to do for game development what Figma did for design.

Unlike Unity or other traditional engines, Dreamlab requires no downloads and is fully collaborative. It has a built-in AI assistant that helps you write code for your game or generate assets. You can send someone a link to your edit session and they can collaborate with you in real-time! Then, you can deploy your game with our built-in multiplayer server.

If you're interested in following along, be sure to star us on GitHub or join us on Discord.