Making games on the web these days is incredibly easy, so let's make it difficult (relatively) by using WebGPU!
A 2D game shouldn't be too difficult, right?
Unfortunately, because we're starting with nothing but WebGPU, we also need a game engine to do anything. So let's make one in TypeScript!
Game Concept
It's just Doro doing random things. I love Doro. Doro is Love. Doro is Life. Doro is Eternal. Doro has descended to bless this world.
Anyway, I'm more of an engine programmer than a game developer in most cases, so we'll see what happens with this project.
Stuff needed from/for/using WebGPU
- Vertex Buffers
- Vertex Shader
- Fragment Shader
- Texture Sampling
- Uniforms
- Matrix Math
- Instancing
My Tech "Stack"
- Supabase: DB, WebSockets, "Backend", Auth
- WebGPU: Rendering, and possibly Compute
- Angular: Frontend Client, and Routing
- Vercel: Hosting
Engine Stuff
- Input System
- Tilemap Editor
- 2D Rendering (WebGPU)
- Sprite Sheet Animation
- Some kind of Behaviour Architecture (ECS, etc.)
- Event Bus/PubSub System
Game Mechanics
- Top Down ARPG (kinda)
- Incremental Gameplay of some kind
- Base Building
- Skill Tree and stuff
- Doro Generator (Character Customization)
How to manage scope?
Who knows, just do a bit every day.
Demo
Check it out here: Tower Of Doro
Source Code
Check out the public repository here: Github
WebGPU-relevant source: renderer.ts
Dev Log
May 29 2025 - 500 lines of code later, we have a spinning doro:
At this point, we have a working basic WebGPU renderer, and can draw single images to the screen. It is however, extremely rudimentary.
June 6 2025 - Primitive Input and Tilemap:
With the help of instancing and storage buffers, we now have a working Tile Map. I also added some simple input to move the player sprite around.
For now, it's all just grass tiles.
June 12 2025 - Sprite Sheet Animation
Many files later.. and a lot of refactoring/abstraction:
Doro can now animates, poggers.
At this point, DoroEngine™ has behaviour systems, Sprite Sheets, Input, 2D Rendering, etc. Not too shabby.
Side Note: I can't figure out how to get rid of the browser keyboard sounds in code, so I'll just live with it I guess.
Stuff used for 2D Animation (so far)
- Texture Atlas for sprite sheet animation
- UV Offsets
- "ECS" System for game logic
Stuff I wanted to use but failed to
- Texture Arrays: Either WebGPU doesn't support texture arrays or a '2d-array' texture view isn't what I think it is, cause it just doesn't work.
June 20 2025 - Tile Map Editor
After a lot of fiddling with matrix math, a lot of refactoring, abstracting, and finally getting orthographic projection to work properly, we now have a working tile map editor. Saved using localStorage (for now).
It also has panning (not shown in video), also the black borders around each tile are for debugging purposes, to ensure that each tile is actually aligned correctly.
Getting the math working for this was a lot more work than expected, firstly because memory padding is a pain to think about, but also because I was too lazy to implement projection initially.
Orthographic Projection actually makes it a lot easier to reason about the math in code. Partially because I chose to convert my engine transform coordinates from clip-space (which it was initially), to pixel space or screen space.
As a result of that, I can easily define one unit, or one tile, as a fixed pixel size, e.g. 64px. Using this unit consistently makes the math a lot less painful to work through.
June 22 2025 - Authentication and User Profile
With the help of Supabase, we now have user accounts, I contemplated using SpacetimeDB, and Spring for the backend; cost-wise, Supabase seems to be the best choice.
Also, I really don't feel like touching Kubernetes right now.
Home Page (Game)
User Profile
Supabase is probably the least effort to get up and running (for now).
Read more about how to use Supabase with Angular here.
Side Peeve: Using Supabase with Angular
Annoyingly, Supabase throws errors when working with Angular's default change detection, which uses zone.js.
Disabling this, and using zoneless change detection, grants the developer more control over when components update, but also results in more verbose code, for better or worse.
What's next?
Some actual gameplay probably?
I've decided to start with some simple incremental gameplay.
Doro collects Doro Particles™ (DP) from the void passively, and can upgrade her Doro Drive™ to collect DP faster.
After that, I'll probably implement some simple chat and multiplayer using WebSockets.
Update: Nevermind.
July 3rd 2025 - Going 2.5D After some deliberation, I've decided to upgrade the engine to do full-on 3D. The art direction for the game however, will remain somewhat pixel-style, going towards something like Octopath Traveller or Tribe Nine.
Most likely, characters will remain 2D, using billboarding to always face the camera. The environment will be 3D, using pixel-style textures. In some ways, this greatly simplifies environment design for me.
Either way, to start with this change, I have to add perspective projection to my current rendering pipeline.
August 2nd 2025 - WASM
After a few weeks of sickness, crunching time working on Balaspire, a lot of Minecraft, horse game addiction, and a general reluctance to deal with model loading, I've finally returned to this project.
Up next is loading models, because what's the point of going 3D if you're just going to use primitive cubes.
My chosen method for loading models is using the GLTF file format. Unfortunately, this does mean that I need to somehow find or write a GLTF Loader.
ThreeJS
At first, I thought of using the three.js GLTFLoader, but then it does seem kind of overkill to install the entire three.js package just to use its loader.
Also, the entire point of this project is to use WebGPU from scratch instead of relying on three.js, soooo, I decided to go with another approach.
C++
I thought of using some standalone GLTF loader package, but then an idea hit me, why not use a C++ loader, which is likely far more optimized anyway.
To do so, I need to build the C++ source to WASM, probably with some binding code in between.
TinyGLTF
My final choice of loader landed on TinyGLTF.
Now, TinyGLTF does have an option to compile to WASM, however, it's using WASI, which doesn't provide JavaScript bindings.
I don't know enough about WASM to determine if WASI will work for my project, but it seems like a real pain.
So instead, let's use Emscripten to create our own port, you can find my experimental WIP implementation here.
TODO:
A lot..
STAY TUNED FOR THE BEST DORO GAME YOU'VE EVER SEEN (from me).