Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions .github/workflows/node.js.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@ name: Node.js CI

on:
push:
branches: [ "main" ]
branches: [ "main", "dev" ]
pull_request:
branches: [ "main" ]
branches: [ "main", "dev" ]

jobs:
build:

runs-on: ubuntu-latest
runs-on: ${{ matrix.os }}

permissions:
# Required to checkout the code
Expand All @@ -18,6 +18,7 @@ jobs:
strategy:
matrix:
node-version: [24.x]
os: [ubuntu-latest, windows-latest]

steps:
- uses: actions/checkout@v4
Expand Down Expand Up @@ -58,3 +59,8 @@ jobs:
npm run build
npm run lint
working-directory: ./packages/multiplayer-template

- name: Start dev servers and verify startup
timeout-minutes: 1
run: npx concurrently --kill-others --success first "npm run dev" "npx wait-on http://localhost:8050/api/game/health --timeout 20000"
working-directory: ./packages/multiplayer-template
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,4 @@ dist-ssr
.env
coverage
*.tsbuildinfo
packages/core/src/version.ts
66 changes: 53 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,35 +1,75 @@
# MavonEngine
# MavonEngine — Three.js Game Engine for Browser Games

A TypeScript game engine with support for single and multiplayer games with an authoritative server.
MavonEngine is an open-source Three.js game engine built for single player or real-time multiplayer. It combines
rendering, physics, networking, animation, and debugging into a single cohesive TypeScript package
— so you're not assembling a stack, you're building a game.

> **Early WIP** — things will change. For documentation visit [mavonengine.com](https://mavonengine.com).

## Getting started

## Get Started
```bash
npx @mavonengine/create-bootstrap
```

| URL | Description |
|-----|-------------|
| --- | --- |
| http://localhost:5173/ | Client Game |
| http://localhost:8050/api/game/health | Server health endpoint |

The multiplayer template includes the authoritative server setup, networking boilerplate, and a working client/server split out of the box.
The multiplayer template includes the authoritative server setup, networking boilerplate, and a
working client/server split out of the box.

## What is MavonEngine?

MavonEngine is a full-stack Three.js game engine built on top of [Three.js](https://threejs.org/)
for rendering, [Rapier3D](https://rapier.rs/) for physics, and
[geckos.io](https://github.com/geckosio/geckos.io) for WebRTC-based UDP networking.

Most Three.js game engines focus on single-player or leave multiplayer as an exercise for the
developer. MavonEngine is designed from the ground up for multiplayer — with a unified server-client
architecture, authoritative physics, and real-time networking built in, not bolted on.

## How It Works

The core is a headless `BaseGame` class that both client and server extend from. The server runs
physics and the game loop without rendering. The client extends it with Three.js rendering, camera,
audio, and input. Because both sides share the same entity classes and loop, game logic is written
once and shared — the authoritative server syncs state down to clients each tick.

The server runs a simplified hitbox scene alongside the client's full 3D world, enabling
server-side hit detection, raycasting, and spatial queries without trusting the client. This makes
it viable for competitive PvP games, open-world multiplayer, and physics-based action games.

## What is this?
## Networking

MavonEngine is a TypeScript game engine built on top of [Three.js](https://threejs.org/) for rendering, [Rapier3D](https://rapier.rs/) for physics, and [geckos.io](https://github.com/geckosio/geckos.io) for WebRTC-based UDP networking. It provides a set of abstractions for the things that come up repeatedly when building 3D games — entity management, state machines, world/chunk streaming, input handling, resource loading, and multiplayer networking.
The networking model is inspired by Source engine architecture — a tick-based command buffer with
server-authoritative state, distance-based entity culling, hash-based change detection, and
bandwidth tracking. The client maintains a WebRTC connection via geckos.io with ping monitoring
and state reconciliation.

## Core idea
## Features

The central design is a headless `BaseGame` class that both the client and server extend from. The server runs the headless version (physics + game loop, no rendering), while the client extends it with Three.js rendering, camera, audio, and input. Because both sides share the same underlying loop and entity classes, game logic can be written once and shared, with the authoritative server syncing state down to clients each tick.
- **Three.js rendering** — Custom GLSL shader support, wireframe/armature/physics debug overlays
- **Rapier3D physics** — Kinematic character controller, collider management, real-time debug visualization
- **WebRTC networking** — Real-time data channels, bandwidth monitoring, command buffering
- **Shared entity system** — Write once, run on server and client
- **Skeletal animation** — GLTF + Draco support, smooth fade transitions, efficient skeleton cloning
- **Particle system** — Built-in rain and smoke effects, custom shader support
- **Level editor** — Early WIP, loads directly from the running game instance
- **Prefab registry** — Community-built assets (grass, water, and more) ready to drop into your scene
- **Full TypeScript** — Comprehensive types throughout

## Multiplayer
## Why a Three.js Game Engine with Multiplayer?

The networking model is inspired by Source engine networking — a tick-based command buffer queue with server-authoritative state, distance-based entity visibility, and bandwidth tracking. The client manages a WebRTC connection via geckos.io with ping monitoring and state reconciliation.
Three.js is the most widely used 3D library for the web, but it's a rendering library — not a game
engine. MavonEngine fills that gap specifically for single or multiplayer: entity management, state machines,
world/chunk streaming, input handling, resource loading, and authoritative networking are all
handled for you.

## Contributing

This is early-stage software built out of real project needs. APIs will break and some parts are still tightly coupled to specific project setups. Contributions and PRs are welcome — see the [contributing guide](https://mavonengine.com/getting-started/contributing) to get started.
This is early-stage software built out of real project needs. APIs will change and some parts are
still tightly coupled to specific setups. Contributions and PRs are welcome — see the
[contributing guide](https://mavonengine.com/getting-started/contributing) to get started.

Join the [community](https://mavonengine.com/community) for development discussions.
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion packages/bootstrap/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@mavonengine/create-bootstrap",
"type": "module",
"version": "0.0.4",
"version": "0.0.8",
"description": "Bootstrap a MavonEngine multiplayer project",
"license": "MIT",
"bin": {
Expand Down
2 changes: 1 addition & 1 deletion packages/bootstrap/src/config.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export const TAG_URL
= 'https://github.com/MavonEngine/Core/archive/refs/tags/0.0.9-alpha.zip'
= 'https://github.com/MavonEngine/Core/archive/refs/tags/0.0.13-alpha.zip'

export const TEMPLATE_SUBPATH = 'packages/multiplayer-template'
75 changes: 75 additions & 0 deletions packages/core/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# MavonEngine — Three.js Game Engine for Browser Games

MavonEngine is an open-source Three.js game engine built for single player or real-time multiplayer. It combines
rendering, physics, networking, animation, and debugging into a single cohesive TypeScript package
— so you're not assembling a stack, you're building a game.

> **Early WIP** — things will change. For documentation visit [mavonengine.com](https://mavonengine.com).

## Get Started
```bash
npx @mavonengine/create-bootstrap
```

| URL | Description |
| --- | --- |
| http://localhost:5173/ | Client Game |
| http://localhost:8050/api/game/health | Server health endpoint |

The multiplayer template includes the authoritative server setup, networking boilerplate, and a
working client/server split out of the box.

## What is MavonEngine?

MavonEngine is a full-stack Three.js game engine built on top of [Three.js](https://threejs.org/)
for rendering, [Rapier3D](https://rapier.rs/) for physics, and
[geckos.io](https://github.com/geckosio/geckos.io) for WebRTC-based UDP networking.

Most Three.js game engines focus on single-player or leave multiplayer as an exercise for the
developer. MavonEngine is designed from the ground up for multiplayer — with a unified server-client
architecture, authoritative physics, and real-time networking built in, not bolted on.

## How It Works

The core is a headless `BaseGame` class that both client and server extend from. The server runs
physics and the game loop without rendering. The client extends it with Three.js rendering, camera,
audio, and input. Because both sides share the same entity classes and loop, game logic is written
once and shared — the authoritative server syncs state down to clients each tick.

The server runs a simplified hitbox scene alongside the client's full 3D world, enabling
server-side hit detection, raycasting, and spatial queries without trusting the client. This makes
it viable for competitive PvP games, open-world multiplayer, and physics-based action games.

## Networking

The networking model is inspired by Source engine architecture — a tick-based command buffer with
server-authoritative state, distance-based entity culling, hash-based change detection, and
bandwidth tracking. The client maintains a WebRTC connection via geckos.io with ping monitoring
and state reconciliation.

## Features

- **Three.js rendering** — Custom GLSL shader support, wireframe/armature/physics debug overlays
- **Rapier3D physics** — Kinematic character controller, collider management, real-time debug visualization
- **WebRTC networking** — Real-time data channels, bandwidth monitoring, command buffering
- **Shared entity system** — Write once, run on server and client
- **Skeletal animation** — GLTF + Draco support, smooth fade transitions, efficient skeleton cloning
- **Particle system** — Built-in rain and smoke effects, custom shader support
- **Level editor** — Early WIP, loads directly from the running game instance
- **Prefab registry** — Community-built assets (grass, water, and more) ready to drop into your scene
- **Full TypeScript** — Comprehensive types throughout

## Why a Three.js Game Engine with Multiplayer?

Three.js is the most widely used 3D library for the web, but it's a rendering library — not a game
engine. MavonEngine fills that gap specifically for single or multiplayer: entity management, state machines,
world/chunk streaming, input handling, resource loading, and authoritative networking are all
handled for you.

## Contributing

This is early-stage software built out of real project needs. APIs will change and some parts are
still tightly coupled to specific setups. Contributions and PRs are welcome — see the
[contributing guide](https://mavonengine.com/getting-started/contributing) to get started.

Join the [community](https://mavonengine.com/community) for development discussions.
9 changes: 6 additions & 3 deletions packages/core/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@mavonengine/core",
"type": "module",
"version": "0.0.8",
"version": "0.0.19",
"description": "",
"author": "",
"license": "MIT",
Expand Down Expand Up @@ -29,12 +29,15 @@
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"files": [
"README.md",
"dist",
"vite",
"vite.config.js"
],
"scripts": {
"build": "rm -f tsconfig.build.tsbuildinfo && tsc -p tsconfig.build.json && find src -name '*.glsl' -o -name '*.css' | while read f; do mkdir -p dist/$(dirname ${f#src/}) && cp $f dist/${f#src/}; done",
"dev": "rm -f tsconfig.build.tsbuildinfo && tsc -p tsconfig.build.json --watch",
"prepare": "node scripts/generate-version.js",
"build": "node scripts/generate-version.js && rm -f tsconfig.build.tsbuildinfo && tsc -p tsconfig.build.json && find src -name '*.glsl' -o -name '*.css' | while read f; do mkdir -p dist/$(dirname ${f#src/}) && cp $f dist/${f#src/}; done",
"dev": "node scripts/generate-version.js && rm -f tsconfig.build.tsbuildinfo && tsc -p tsconfig.build.json --watch",
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"bench": "vitest bench",
Expand Down
11 changes: 11 additions & 0 deletions packages/core/scripts/generate-version.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { readFileSync, writeFileSync } from 'node:fs'
import { dirname, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'

const __dirname = dirname(fileURLToPath(import.meta.url))
const { version } = JSON.parse(readFileSync(resolve(__dirname, '../package.json'), 'utf8'))

writeFileSync(
resolve(__dirname, '../src/version.ts'),
`export const version = '${version}'\n`,
)
2 changes: 1 addition & 1 deletion packages/core/src/BaseGame.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import type winston from 'winston'
import type Logger from './Utils/Logger'
import type GameObjectInterface from './World/GameObjectInterface'
import { Clock, Raycaster, Scene } from 'three'
import { version as ENGINE_VERSION } from '../package.json' with { type: 'json' }
import EventEmitter from './Utils/EventEmitter'
import { version as ENGINE_VERSION } from './version'

import BaseWorld from './World/BaseWorld'

Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/Networking/Entities/Player.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export default abstract class Player extends NetworkedLivingActor {

trackedEntities = new Set<string>()

public serialize(): object {
public serialize() {
return {
...super.serialize(),
$typeName: this.$typeName,
Expand Down
26 changes: 26 additions & 0 deletions packages/core/src/Networking/NetworkedEntityFactory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import type NetworkedActor from './NetworkedActor'

type EntityFactory = (data: Record<string, unknown>) => NetworkedActor
const registry = new Map<string, EntityFactory>()

export default class NetworkedEntityFactory {
private static _instance: NetworkedEntityFactory

static get instance(): NetworkedEntityFactory {
if (!NetworkedEntityFactory._instance) {
NetworkedEntityFactory._instance = new NetworkedEntityFactory()
}

return NetworkedEntityFactory._instance
}

register(typeName: string, factory: EntityFactory) {
registry.set(typeName, factory)
}

create(typeName: string, data: Record<string, unknown>): NetworkedActor | null {
const factory = registry.get(typeName)

return factory ? factory(data) : null
}
}
11 changes: 9 additions & 2 deletions packages/core/src/Networking/NetworkedLivingActor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,18 @@ export default abstract class NetworkedLivingActor extends NetworkedGameObjectMi
}
}

public serialize(): object {
public serialize(): {
id: string
position: Vector3
rotation: Vector3
scale: Vector3
health: number
state: NetworkedEntityState[]
} {
return {
...super.serialize(),
state: this.state,
}
} as ReturnType<NetworkedLivingActor['serialize']>
}

isDead(): boolean {
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/Networking/Server/Server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ export default abstract class Server<TClient extends GameObject> {
})

channel.on('command', (data) => {
const bytes = Buffer.byteLength(data.toString())
const bytes = Buffer.byteLength(JSON.stringify(data))
this.bandwidthTracker.recordReceived('server', bytes)

this.bufferIncomingCommand(channel, data)
Expand Down Expand Up @@ -174,7 +174,7 @@ export default abstract class Server<TClient extends GameObject> {
type: ServerCommand.SV_STATE,
}

this.bandwidthTracker.recordSent('server', Buffer.byteLength(state.toString()))
this.bandwidthTracker.recordSent('server', Buffer.byteLength(JSON.stringify(state)))
con.channel.emit(ServerCommand.SV_STATE, state)
})

Expand Down
4 changes: 0 additions & 4 deletions packages/core/src/World/Chunk.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
import GameObject from './GameObject'

export default class Chunk extends GameObject {
public serialize(): object {
throw new Error('Method not implemented.')
}

static CHUNK_SIZE: number

constructor(x: number, y: number) {
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/World/GameObject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export default abstract class GameObject extends Entity implements GameObjectInt

abstract update(delta: number): void

public serialize(): object {
public serialize() {
return {
id: this.id,
position: this.position,
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/World/LivingActor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export default abstract class LivingActor extends Actor implements LivingEntity
abstract takeDamage(amount: number): void
abstract heal(amount: number): void

public serialize(): object {
public serialize() {
return {
...super.serialize(),
health: this.health,
Expand Down
Loading
Loading