I built a lightweight 2D Web Game Engine in Pure Vanilla JS (BeeEngine v2.8.4)
Hi DEV community! π Over the past few months, I've been working on BeeEngine, a lightweight 2D game engine built with pure Vanilla JavaScript and HTML5 Canvas. My main goal was to create a clean, modular engine withou
Hi DEV community! π
Over the past few months, I've been working on BeeEngine, a lightweight 2D game engine built with pure Vanilla JavaScript and HTML5 Canvas.
My main goal was to create a clean, modular engine without relying on heavy external frameworks or complex build setups. It features a custom built-in visual inspector (BeeLadybug), a State-Graph Animator, and a robust Audio Mixer.
π οΈ Project Structure
Here is how BeeEngine is structured under the hood:
BeeEngine-V2.8/
βββ index.html # Entry point & Canvas config
βββ index.js # ESM Barrel (re-exporting BeeEngine.js)
βββ main.js # Visual Demo (BeeUI: anchor, stack, focus, HUD)
βββ BeeEngine.js # CORE (Core Loop & System Coordinator)
βββ index.d.ts # Global TypeScript definitions & IntelliSense
βββ src/
βββ audio/ # BeeAudioMixer (bus, fade, duck, pan 2D)
βββ core/ # Entities, Prefabs, Tweens, Timelines, Pooling, Scene Manager
βββ gameplay/ # Player, Enemy, Platform, Collectibles, Menus
βββ graphics/ # Layers, Animator, Camera, Tilemaps, Particles
βββ input/ # Keyboard, Mouse, Touch & Virtual Joysticks
βββ ui/ # BeeUI (Panels, Stacks, Labels, Buttons, Nine-slice)
βββ physics/ # Spatial Hashing, RigidBody & AABB Collisions
βββ debug/ # BeeLadybug (Visual Inspector & Hitbox Overlay)
β¨ Key Features
- Zero External Dependencies: Built with 100% pure Vanilla JS.
- Physics & Collision: Integrated Spatial Hash grid for efficient AABB collision detection.
- Input System: Built-in support for Keyboard, Mouse, Touch, and Virtual Joysticks.
- UI Engine (BeeUI): Flex-like layouts, anchors, focus management, and nine-slice UI rendering.
- Visual Debugger (BeeLadybug): Built-in tool for real-time inspection, data tracking, and hitbox rendering.
π¬ Deep Dive: BeeAnimator (State Graph, Not Just Clips)
Most simple engines just play an animation clip. BeeAnimator is a proper State Graph Coordinator. While BeeAnimatedSprite renders the frames, the Animator decides which clip to play and when, handling priorities, hit-locks, and automatic transitions.
The entity.animator ticks inside the entity loop using the simulation's delta time (dt), meaning if the game pauses, the animation lifecycle freezes correctly.
// 1. Create the base animated sprite (The Renderer)
const sprite = gioco.createAnimatedSprite(sheet, {
animations: {
idle: { frames:, fps: 4, loop: true },
run: { frames:, fps: 10, loop: true },
attack: { frames:, fps: 12, loop: false }
}
});
// 2. Wrap it in the State Graph Animator
const animator = gioco.createAnimator(sprite);
animator
.add('idle', { clip: 'idle', initial: true })
.add('run', { clip: 'run', priority: 1 })
.add('attack', { clip: 'attack', loop: false, lock: true, priority: 10, exitTo: 'idle' })
// State Transitions via Predicates
.when('idle', 'run', (actor) => Math.abs(actor.vx) > 1)
.when('run', 'idle', (actor) => Math.abs(actor.vx) <= 1)
.when('*', 'attack', (actor) => actor.wantsAttack)
.start();
actor.sprite = sprite;
actor.animator = animator;
Core Concepts:
| Feature | Mechanics |
|---|---|
| lock | As long as the clip isn't finished, states with priority (\le) current cannot interrupt it (they get queued). |
| exitTo | Defines where to transition automatically once a one-shot/locked clip finishes. |
| when('*', to, pred) | Defines a global transition predicate checkable from any active state. |
| play(name, { force }) | Manual override request; force: true breaks active animation locks. |
Note: BeeAnimatedSprite.play(name, { restart: true }) and sprite.finished are exposed so the animator knows precisely when an action (like an attack) ends.
π Deep Dive: BeeAudioMixer (Audio Nodes & Bus Hierarchy)
Instead of poorly cloning HTML5 audio nodes (cloneNode), BeeEngine uses a proper audio graph node system via BeeAudioMixer. It handles a full hierarchy: master (\rightarrow) music / sfx / ui / voice. It supports volume control, muting per channel, smooth fading, 2D panning relative to the camera listener, and automatic audio ducking.
The mixer ticks every frame using real-time execution independent of the game simulation pause state.
// Unlock Audio Graph (requires user gesture interaction)
gioco.audio.unlock();
// Play background music with crossfade and custom volume
gioco.audio.music(track, { fade: 0.6, volume: 0.5 });
// Play a 2D localized sound effect spatialized vs the camera listener
gioco.audio.play('hit', { bus: 'sfx', x: 120, y: 300 });
// Procedural beep generation (perfect for web demos without downloading MP3s)
// Triggers automatic music ducking because it runs on the 'voice' bus
gioco.audio.tone({ frequency: 180, duration: 1.1, bus: 'voice' });
// Global Bus Controls
gioco.audio.setVolume('sfx', 0.8);
gioco.audio.fade('music', 0.2, 0.4);
Core Concepts:
| Feature | Mechanics |
|---|---|
| bus | Hierarchy chain controller. Muting a parent bus automatically silences all child streams. |
| duck | Default behavior: Active signals on the voice bus automatically duck the music bus volume down to 0.35. |
| x, y | Automatic distance attenuation and left/right stereo panning relative to the listener position. |
| tone() | Low-overhead procedural beep generator for quick sound design prototyping. |
π¦ Try It Out
You can include BeeEngine directly in your project via npm or CDN:
jsDelivr CDN:
<script src="https://jsdelivr.net"></script>
npm:
npm install beeengine
π CDN Stats & Package: BeeEngine on jsDelivr
I'd love to hear your thoughts, feedback, or suggestions from fellow web developers and gamedevs! What features would you like to see next?
Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes β full credit and traffic to the original publisher.