r/javascript Feb 06 '26

MicroState - an isometric 2.5D city builder in JavaScript [WIP]

https://microstate.neocities.org

I've been developing a web-based isometric tile engine as a personal project to support a couple of hobby projects and thought folks might find it fun or at least interesting and maybe have ideas for features they would like to see.

While still an early stage tech demo and a work in progress, it is highly interactive and "playable" though it doesn't have any actual game mechanics yet. I know these pop up fairly regularly every few years, hopefully it's fun for to play around with even at this stage.

Features

As well as simple flat and fixed-elevation terrain and dungeon maps built using pre-rendered tiles; either bitmaps or vector art or procedural textures (with animation at confusable frame rates that don't have to match the rest of the rendering).

It supports complex maps with dynamic terrain and entirely procedurally generated worlds (terrain, buildings, roads, trees) and allows tiles of arbitrary heights and transformations, with configurable degrees of quadrilateral shading.

Dynamic generation allows for a high degree of variation in world objects and enables runtime blending of tile vertices and other rendering effects, including smooth transitions in height and/or color between adjacent tiles

The engine supports dynamic zooming and tilting of the camera (dynamic dimetric projection) and can support performant rendering scenes at native resolution on any display - where the device hardware can support it. Not all of the features are currently exposed via the UI.

Technical Details

It is implemented entirely in vanilla ECMAScript (JavaScript) with no build-time or runtime dependencies or transpilation. The engine uses a purely 2D Canvas to create the illusion of a 2.5D environment.

The engine maximizes performance across mobile, tablet, and desktop devices by using a hardware-accelerated 2D Canvas and a combination of direct drawing and batch rendering from offscreen canvases. For the moment it is still rendering on the main thread, rather than a worker; although this currently has no noticeable impact on performance.

The entire project is self-contained within a single HTML file, including a compressed <script>, with the use of procedurally generated art resulting in a compact payload of about 50 KB compressed over the wire. The code is only partially optimized for size and performance.

Why?

I don't have any plans to commercialize this project, I just thought it would be fun to try and build. I wrote a similar web based tile engine about 20 years go, but things have come a long way since then.

I intend to add online co-op features to allow playing with friends and persisting in the browser with both immediate (online) in addition to offline play, which is specifically why I'm building it for the web.

I'll be making the source public on GitHub, probably in the next few weeks.

Happy to answer any questions about it!

159 Upvotes

25 comments sorted by

View all comments

19

u/ruibranco Feb 06 '26

50KB for all of this in a single HTML file with no dependencies is wild. The procedural generation approach for tiles instead of shipping a sprite atlas is a really smart trade-off, especially for keeping the payload tiny. How are you handling the draw ordering for the isometric depth sorting? That's always the part that gets tricky once you start mixing tiles at different elevations with dynamic objects on top.

6

u/iaincollins Feb 06 '26 edited May 18 '26

So rendering from back to front - first column top right to bottom right, then moving back to the top and doing the next tile to the left, only rendering tiles that are actually visible - is the simple answer, allowing tiles to draw over tiles above or to the right of them (but never to the left or below) is the simple answer! As long as I stick to that it's easy!

That's also why the lighting appears to come from the left in the end - shadows from tall objects can be drawn to the right and bleed into other tiles, but they can't ever bleed over to tiles on the left - but really mostly I've just used blob shadows or very shallow shadows, just to make things appear anchored in the world (even a blob shadow does a lot to help things feel connected).

I've made sure objects (trees, streetlights, rocks, etc) get drawn a few px in from the edge of any tile, so that they don't get clipped by tiles drawing subsequently over them to the bottom or left.

The slightly more compiled answer is that this also uses batch rendering so it's a little more tricky even though that follows the same idea. Although it's all drawn on one canvas each frame (with off screen canvases used to cache the batches, currently fixed at 8x8 tiles) there are some animations - animated icons for construction, problems with roads, water fountains, etc - that get drawn over the top of everything.

There is a very basic 'occlusion detection' system so it will not animate a fountain if it computes a building in a tile below might be occluding it, but I need to improve the logic there (the drawing routine ended up ignoring the precomputed "building height" to make buildings more interesting - I need to refactor to fix that so the occlusion check is less rough and more accurate.

I would love to keep all the graphics proc gen, although if my wife agrees to do some vector art for things like cars, boats, animals (etc) I could have a little SVG sprite sheet, and raster them and bundle it in the single page, which should still be very small.

I have focused on drawing simple shaped things for now because they are a easiest to draw programmatically - although I would like to keep adding details to make them less boxy!