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?
Top comments (0)