DEV Community

Antonio
Antonio

Posted on

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 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)
Enter fullscreen mode Exit fullscreen mode

✨ 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;
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

npm:

npm install beeengine
Enter fullscreen mode Exit fullscreen mode

📊 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)