A from-scratch 2D ARPG engine — raw WebGL, zero game-framework dependencies
Miu2D is a 176,000-line 2D ARPG engine written in TypeScript and Rust, rendering through raw WebGL with no dependency on Unity, Godot, Phaser, PixiJS, or any other game framework. Every subsystem — sprite batching, A* pathfinding, binary format decoders, scripting VM, weather particles, screen effects — is implemented from first principles.
As a proof of concept, Miu2D has been used to rebuild three classic Kingsoft (西山居) wuxia RPGs, all fully playable in any modern browser.
Vibe Coding — This project is developed with AI-assisted programming from day one.
| Developer | Xishanju (西山居 / Kingsoft) |
|---|---|
| Genre | Action RPG |
| Highlights | 7+ endings · 100+ story events · 30-person team (20+ artists) · 14-month production |
The largest production Xishanju had ever mounted at the time. The story branches dramatically based on player choices — loyalty, love, wealth — shaping the protagonist's morality and emotional alignment to produce seven or more distinct endings. Scenes were built with a pioneering 3D+2D hybrid rendering technique, and the soundtrack blended classical Chinese instruments with contemporary pop production.
| Developer | Xishanju (西山居 / Kingsoft) |
|---|---|
| Genre | Action RPG |
| Highlights | Diablo-style real-time combat · 200+ NPCs · 640×480 16-bit color · theme song by 谢雨欣 |
Set twenty years after the original, hero 南宫飞云 — son of the first game's protagonists — stumbles into a perilous journey after rescuing a mysterious girl named 若雪. The sequel boldly abandoned turn-based combat for a real-time action system inspired by Diablo, a revolutionary move for Chinese RPGs of the era. Three years in development with a budget of nearly ¥3 million and a team of 30.
| Developer | Xishanju (西山居 / Kingsoft) |
|---|---|
| Genre | Action RPG |
| Highlights | Remake of the 1997 original · 110+ maps · indoor map system · real-time combat engine from Swords 2 |
A remake of the franchise's 1997 debut, rebuilt with the acclaimed real-time action combat engine from Swords of Legends 2. The story remains faithful to the original while greatly expanding the map count to 110+ scenes and introducing seamless indoor/outdoor transitions.
Mobile & Editor Screenshots
Mobile — virtual joystick + touch controls:
Map Editor — visual tilemap editing, collision zones:
ASF Editor — sprite animation frame viewer & debugger:
Real-time Lighting & Shadows — additive glow + SHD shadow rendering:
Most web game projects reach for PixiJS, Phaser, or a WASM-compiled Unity/Godot build. Miu2D takes a different path: the entire rendering pipeline talks directly to WebGLRenderingContext, the pathfinder lives in Rust compiled to WASM with zero-copy shared memory, and the scripting engine supports both 218 DSL commands through a custom parser/executor pair and a full Lua 5.4 runtime (via wasmoon) sharing the same GameAPI. The result is a system whose every layer is visible, debuggable, and tailored to 2D RPG mechanics.
What this buys you:
- Full control over the render loop — a
SpriteBatchercoalesces ~4,800 map tile draws into 1–5 WebGL draw calls; aRectBatcherreduces ~300 weather particles to a single call; a real-time lighting pass composites per-entity additive glow masks with SHD-based shadow rendering. - No abstraction tax — no unused scene graph, no 3D math overhead, no framework event model to work around.
- Rust-speed where it matters — A* pathfinding runs in ~0.2 ms via WASM with obstacle data written directly into linear memory (no serialization, no FFI copy).
- Clean architecture for study — an 8-level class hierarchy (Sprite → CharacterBase → Movement → Combat → Character → PlayerBase → PlayerCombat → Player) with clear separation of concerns, ideal for understanding how a full 2D RPG engine works under the hood.
| Layer | Package | Details |
|---|---|---|
| UI | @miu2d/game |
React 19 · 3 themes (Classic / Modern / Mobile) · 84 components |
| Engine | @miu2d/engine |
Pure TypeScript · 215 files · 19 modules · no React dependency |
| ↳ Renderer | renderer/ |
Raw WebGL · SpriteBatcher · Canvas2D fallback · GLSL filters |
| ↳ Script VM | script/ |
218 commands · custom parser + async executor · Lua 5.4 (wasmoon WASM) |
| ↳ Character | character/ |
8-level inheritance chain · NPC AI · bezier movement |
| ↳ Magic | magic/ |
22 MoveKind trajectories · 10 SpecialKind effects |
| WASM | @miu2d/engine-wasm |
Rust → WebAssembly · A* pathfinder · decoders · SpatialHash · zstd |
| Backend | @miu2d/server |
Hono + tRPC + Prisma ORM · 21 PostgreSQL tables · 19 routers |
| Editor | @miu2d/dashboard |
VS Code-style layout · 13 editing modules |
| Layer | Technology |
|---|---|
| Language | TypeScript 5.9 (strict) · Rust · GLSL |
| Frontend | React 19 · Vite 8 (rolldown) · Tailwind CSS 4 |
| Rendering | Raw WebGL API (Canvas 2D fallback) |
| Audio | Web Audio API (OGG Vorbis) |
| Performance | Rust → WebAssembly (wasm-bindgen, zero-copy) |
| Backend | Hono (lightweight HTTP) · tRPC 11 · Prisma ORM |
| Database | PostgreSQL 16 · MinIO / S3 |
| Validation | Zod 4 (shared schemas across client & server) |
| Quality | Biome (lint + format) · TypeScript strict mode |
| Monorepo | pnpm workspaces (11 packages) |
Miu2D implements 17 integrated ARPG subsystems (218 script commands) entirely from first principles:
| System | Module | Highlights |
|---|---|---|
| Rendering | renderer/ |
Raw WebGL sprite batcher (~4,800 tiles → 1–5 draw calls), Canvas2D fallback, GLSL color filters (poison / freeze / petrify), screen effects (fade, flash, water ripple), real-time lighting & shadows (additive lum masks, per-entity glow, SHD-based shadow rendering) |
| Character | character/ |
8-level inheritance chain (Sprite → CharacterBase → Movement → Combat → Character → PlayerBase → PlayerCombat → Player/NPC); stats, status flags, bezier-curve movement |
| Combat | character/ |
Hit detection, damage formula, knockback, death & respawn, party/enemy faction logic |
| Magic / Skill | magic/ |
22 MoveKind trajectories (line, spiral, homing, AoE, summon, time-stop…) × 10 SpecialKind effects; per-level config, passive XiuLian system |
| NPC & AI | npc/ |
Behavior state machine (idle / patrol / chase / flee / dead), interaction scripts, spatial grid for fast neighbor lookup |
| Player | player/ |
Controller, inventory (goods system), equipment slots, magic slots, experience & leveling |
| Map | map/ |
Multi-layer tile parsing, obstacle grid, trap zones, event areas, layer-sorted rendering |
| Script / Event | script/ |
Custom VM: parser + async executor, 218 commands across 9 categories (dialog, player, NPC, state, audio, effects, objects, items, misc); Lua 5.4 scripting via wasmoon WASM with full GameAPI bindings (170 PascalCase functions) |
| Pathfinding | wasm/ |
Rust WASM A* with zero-copy shared memory; 5 strategies (greedy → full A*); ~0.2 ms per query, ≈10× faster than TS |
| Collision | wasm/ |
SpatialHash in Rust/WASM for O(1) broad-phase entity queries |
| Audio | audio/ |
Web Audio API manager: streamed BGM (OGG/MP3), positional SFX (WAV/OGG), fade transitions |
| Weather / Particles | weather/ |
Wind-driven rain + splash + lightning flash; wobbling snowflakes; screen-droplet lens effect |
| Object / Prop | obj/ |
Interactable scene objects (chests, doors, barriers, traps) with script hooks and sprite animation |
| GUI / HUD | gui/ |
Dialog system (branching choices, portraits), shop/buy panel, mini-map, status bars, UI bridge to React |
| Inventory / Items | player/ |
10 goods categories, equip/unequip, use effects, loot drops with configurable drop tables |
| Save / Load | storage/ |
Multiple save slots, full game-state serialization to IndexedDB + server-side cloud saves |
| Resource Loading | resource/ |
Async loader for 8 binary formats (ASF, MPC, MAP, SHD, XNB, MSF, MMF, INI/OBJ); GBK/UTF-8 decoding |
The renderer directly calls WebGLRenderingContext — no wrapper library.
- SpriteBatcher — accumulates vertex data and flushes per texture change; typical map frame: ~4,800 tiles → 1–5 draw calls
- RectBatcher — weather particles and UI rectangles batched into a single draw call
- GPU texture management —
ImageData→WebGLTexturewithWeakMapcaching andFinalizationRegistryfor automatic GPU resource cleanup - GLSL color filters — grayscale (petrification), blue tint (frozen), green tint (poison) applied per-sprite in the fragment shader
- Screen effects — fade in/out, color overlays, screen flash, water ripple, all composited in the render loop
- Canvas 2D fallback — same
Rendererinterface, full feature parity for devices without WebGL - Local lighting (LumMask) — when
SetMainLumdarkens the scene, light-emitting entities (objects, NPCs, magic projectiles) generate an additive white 800×400 elliptical glow mask at their position. A per-tile dedup (matching C++Weather::drawElementLum) prevents double-drawing. AnoLumflag on magic sub-projectiles suppresses redundant light sources for dense spell patterns, accurately matching the C++ reference:- LineMove: 1-in-3 sub-projectiles emit light (
i % 3 === 1) - Square region: 1-in-9 (
i % 3 === 1 && j % 3 === 1) - Wave / Rectangle region: 1-in-4 (
i % 2 !== 0 && j % 2 !== 0)
- LineMove: 1-in-3 sub-projectiles emit light (
The engine supports two scripting modes that share the same GameAPI:
DSL Mode (.txt / .npc) — A custom parser tokenizes game script files; an executor interprets them with blocking/async support. Commands span 9 categories:
| Category | Examples |
|---|---|
| Dialog | Say, Talk, Choose, ChooseMultiple, DisplayMessage |
| Player | AddLife, AddMana, SetPlayerPos, PlayerGoto, Equip |
| NPC | AddNpc, DelNpc, SetNpcRelation, NpcAttack, MergeNpc |
| Game State | LoadMap, Assign, If/Goto, RunScript, RunParallelScript |
| Audio | PlayMusic, StopMusic, PlaySound |
| Effects | FadeIn, FadeOut, BeginRain, ShowSnow, OpenWaterEffect |
| Objects | AddObj, DelObj, OpenObj, SetObjScript |
| Items | AddGoods, DelGoods, ClearGoods, AddRandGoods |
| Misc | Sleep, Watch, PlayMovie, DisableInput, ReturnToTitle |
Lua Mode (.lua) — Full Lua 5.4 runtime via wasmoon (Lua compiled to WASM). All 170 GameAPI functions are exposed as PascalCase Lua globals. wasmoon's proxy system automatically bridges JS async functions to Lua coroutines — blocking operations like PlayerWalkTo() or Talk() just work with co.await-style transparency. Dispatched by file extension; the same ScriptExecutor routes .lua files to LuaExecutor and .txt/.npc files to the DSL executor.
-- Example Lua game script
FadeOut()
LoadMap("map/town.map")
SetPlayerPos(10, 15)
FadeIn()
Talk(0, "Welcome to the village.")
local choice = Choose("Join the quest?", "Yes", "No")
if choice == 1 then
AddMagic("magic/fireball.ini")
AddExp(500)
endScripts drive the entire game narrative — cutscenes, branching dialogs, NPC spawning, map transitions, combat triggers, and weather changes.
Every magic attack follows one of 22 MoveKind trajectories, each with its own physics and rendering:
| Movement | Behavior |
|---|---|
| LineMove | Multi-projectile line — count scales with level |
| CircleMove | Orbital ring pattern |
| SpiralMove | Expanding spiral outward |
| SectorMove | Fan-shaped spread |
| HeartMove | Heart-shaped flight path |
| FollowEnemy | Homing missile tracking |
| Throw | Parabolic arc projectile |
| Transport | Teleportation |
| Summon | Spawn allied NPC |
| TimeStop | Freeze all entities |
| VMove | V-shaped diverging spread |
| ...and 11 more |
Combined with 10 SpecialKind effects (freeze, poison, petrify, invisibility, heal, buff, transform, remove-debuff…), this produces hundreds of unique spell combinations. The system includes specialized sprite factories, a collision handler, and a passive effect manager (XiuLian/修炼).
The A* pathfinder is written in Rust, compiled to WebAssembly. It eliminates all FFI overhead through shared linear memory:
- JavaScript writes obstacle bitmaps directly into WASM linear memory via
Uint8Arrayviews onwasm.memory.buffer - WASM executes A* in-place on shared memory
- JavaScript reads path results via
Int32Arraypointer views — zero serialization, zero copying
Five path strategies (from greedy to full A* with configurable max iterations) let the game trade accuracy for speed. Typical pathfind: ~0.2 ms, roughly 10× faster than the equivalent TypeScript implementation.
The engine parses 8 binary file formats from the original game — all reverse-engineered and implemented without third-party parsing libraries:
| Format | Description |
|---|---|
| ASF | Sprite animation frames (RLE-compressed, palette-indexed RGBA) |
| MPC | Resource pack container (bundled sprite sheets) |
| MAP | Tile map data (multiple layers, obstacle grid, trap zones) |
| SHD | Shadow / height map data for terrain |
| XNB | XNA Binary format (audio assets from the original game) |
| MSF | Miu Sprite Format v2 — custom indexed-palette + zstd compression |
| MMF | Miu Map Format — custom zstd-compressed binary map data |
| INI/OBJ | Config files in GBK (Chinese legacy encoding) and UTF-8 |
Particle physics and rendering:
- Rain — wind-affected particles with splash on contact, periodic lightning flash illuminating the scene
- Screen droplets — simulated refraction/lens effect of water running down the camera
- Snow — individual snowflake physics with wobble, spin, drift, and gradual melt
A deep, well-structured class hierarchy with clear separation of concerns:
Sprite
└─ CharacterBase — stats, properties, status flags
└─ CharacterMovement — A* pathfinding, tile walking, bezier curves
└─ CharacterCombat — attack, damage calc, status effects
└─ Character — shared NPC/Player logic [abstract]
├─ PlayerBase → PlayerCombat → Player
└─ Npc — AI behavior, interaction scripts, spatial grid
The project includes a VS Code-style game editor with Activity Bar, Sidebar, and Content panels:
| Module | What it edits |
|---|---|
| Magic Editor | Spell config with live ASF sprite preview |
| NPC Editor | Stats, scripts, AI behavior, sprite preview |
| Scene Editor | Map data, spawn points, traps, triggers |
| Item Editor | Weapons, armor, consumables, drop tables |
| Shop Editor | Store inventories and pricing |
| Dialog Editor | Branching conversation trees + portrait assignment |
| Player Editor | Starting stats, equipment, skill slots |
| Level Editor | Experience curves and stat growth |
| Game Config | Global game settings (drops, player defaults) |
| File Manager | Full file tree with drag-and-drop upload |
| Resources | Resource browser and viewer integration |
| Statistics | Data overview dashboard |
11 packages in a pnpm monorepo, ~176,000 lines total:
| Package | Role |
|---|---|
@miu2d/engine |
Pure TS game engine — 19 modules, no React dependency |
@miu2d/dashboard |
VS Code-style game data editor (13 modules) |
@miu2d/game |
Game runtime with 3 UI themes (classic/modern/mobile) |
@miu2d/server |
Hono + tRPC backend (21 tables, 19 routers) |
@miu2d/types |
Shared Zod 4 schemas (18 domain modules) |
@miu2d/web |
App shell, routing, landing page |
@miu2d/converter |
Rust CLI: ASF/MPC → MSF, MAP → MMF batch conversion |
@miu2d/engine-wasm |
Rust → WASM: pathfinder, decoders, spatial hash, zstd |
@miu2d/viewer |
Resource viewers (ASF/Map/MPC/Audio) |
@miu2d/ui |
Generic UI components (no business deps) |
@miu2d/shared |
i18n, tRPC client, React contexts |
Also included: resources/ (game assets), docs/ (format specs), JxqyHD/ (C# reference from the original engine).
Requirements: Node.js 18+, pnpm 9+, modern browser with WebGL
git clone https://github.com/nicologies/miu2d.git
cd miu2d
pnpm install
pnpm dev # → http://localhost:5173make init # Docker: PostgreSQL + MinIO, migrate, seed
make dev # web + server + db studio concurrently| Command | Purpose |
|---|---|
pnpm dev |
Frontend dev server (port 5173) |
make dev |
Full-stack dev (web + server + db) |
make tsc |
Type check all packages |
pnpm lint |
Biome lint |
make test |
Run engine tests (vitest) |
make convert |
Batch convert game resources (Rust CLI) |
make convert-verify |
Pixel-perfect conversion verification |
| Input | Action |
|---|---|
| Left click (ground) | Move to position |
| Left click (NPC / object) | Interact |
| Right click (NPC / object) | Alternate interact |
| Ctrl + Left click | Attack in place |
Q |
Interact with nearest object |
E |
Interact with nearest NPC |
A S D F G |
Cast magic (skill slots 1 – 5) |
Z X C |
Use item (quick slots 1 – 3) |
V |
Toggle sitting / meditate (修炼) |
| Input | Action |
|---|---|
| Virtual joystick | Move |
| Tap (NPC / object) | Interact |
| Target | Method |
|---|---|
| Frontend | Vercel — pnpm build:web → static SPA |
| Full Stack | Docker Compose — PostgreSQL + MinIO + Hono + Nginx |
See deploy/ for production Docker configs.
- Fork → feature branch → reference the dev guide → PR
- Run
make tscandpnpm lintbefore submitting
- Original Game: Kingsoft (西山居) — 剑侠情缘外传:月影传说 (2001)
This is a fan-made learning project. Game assets and IP belong to their original creators.
⚔️ Sword spirit spans thirty thousand miles ⚔️
Recreating classic wuxia with modern web technology






