Kingdom Crushers
A cross platform MMO inspired by games like ROTMG and Darza's Dominion.
Tools:
- Godot
- Supabase
- NextJS
- Golang
- AWS
- Stripe
- Digital Ocean
Architecture
We used two servers, the authentication server and the game server. The auth server acts like a hub for all players who want to log in or create a new character, any task you can do in the home page which requires database stuff. We just use classic http requests to get info since there is no real need for real time updates in a lobby, stuff like leaderboards and playercounts are not urgent. The other server is the game server, handling the actual gameplay (collision, player interactions, enemy behaviour). The game server uses websockets, which aren't ideal for an MMO since it's packets have high overhead costs (TCP), but it was necessary since really having this game on the web was priority #1. What about WebTransport? Godot does not support this yet. What about WebRTC? While UDP would be ideal and WebRTC is widely avaliable on browsers, it is designed for p2p meaning it needs some tweaking/problem solving to get working. I have been looking into implementing it, and it seems like you can use a "headless" client which relays data to the server, but currently websockets seem like a safer bet. So running through what a backend session would look like, if a player wants to log in they send a request to the auth server. If the body includes a valid password/email combo then it logs them in, sending their account data as well as a token of validation. A matching token is sent to the game server with their email, so they can use to get into the game server. This login session lasts an hour, so if they just afk on the homepage for an hour they will have to reload the page.
Game
The game servers have a lot to deal with. Since it's a bullet hell, we need to look for collisions at all times between projectiles and objects (objects can be players, monsters, or literal objects like trees). Since it's simply too much work to check each projectile with each object, there are a few ways that you could deal with this. Chunking the world into a grid and then only checking against objects in the same grid is an obvious solution, but not very interesting. What about killing two birds with one stone? Assign each player logged into the game a world state, which includes the objects and projectiles they can see. Any time we spawn a new projectile add it to the world state of all players within a radius, so that if a player just enters a new world they won't get caught by suprise and instakilled by projectiles they couldn't anticipate. Each player object can then simply check for collisions within their world state, elegantly keeping the players view and backend interactions the same.
Enemies themselves are handled similarly. We keep a world state of objects/projectiles, then add projectiles whenever a nearby player shoots. This also allows for faster pathfinding since we only have to account for the objects close by (though this makes the enemies slightly dumber, but thats alright). While in many games enemies are given their own unique scripts to follow, this was not a luxury we could afford with over 100 unique monsters. We needed some way to generalize their behaviour, so we represent them using good old JSON.
Heres an example of a crab enemy, you can see it has a behaviour which translates to how it moves; 0 = idle, 1 = wander, 2 = chase, 3 = orbit
"crab": {
"behavior": 1,
"defense": 1,
"exp": 10,
"health": 50,
"loot_pool": basic_loot,
"phases": [
{
"attack_pattern": [
{
"damage": 2,
"direction": "(1, 0)",
"formula": "0",
"piercing": false,
"projectile": "Slash",
"size": 5,
"speed": 10,
"targeter": "nearest",
"tile_range": 3,
"wait": 1
}
],
"duration": 10,
"health": [
0,
100
]
}
],
"speed": 10
}