In this post, I'm going to describe the client/server "architecture" of my as-yet unnamed RPG, such as it is. The basic premise of the game is that it's an open-world, browser-based role-playing game. My expectation is that there could be ~100 players online at a time, and the game should be able to reasonably handle that on a single server. Currently I have no plans for how to handle expanding the game past a single server, apart from some vague ideas about splitting the world geographically.
With that said, here's a picture of how the server looks today:
From top-to-bottom we have:
- In the server's main process, one "player handler" exists for each connected player. The player handler keeps a reference to the player's entity in the world.
- The "world" is basically a collection of entities and some supporting data structures (e.g. pathing objects for constructing paths, etc) and a bunch of goroutines acting on those entities.
- The "long-term storage" (or just "Store" as it's called in the code) is where we store all the things which need to persist between server restarts (things like player stats, inventory, quest status and so on)
- The "sectors" collection is a bunch of static files which describe a segment of the world. These are 64x64 grids that contain information about the terrain, the entities that should be initially created on the terrain and so on.
Entities are simply a collection of "components". A component provides the basic functionality of the entities, and can be combined to form any kind of complex entity we like. The components used on the server and client are not always the same (for example, the "Model" component is ignored on the server, but it's obviously very important on the client!)
Some of the components we have defined are described below.
- Position - This contains the coordinates of the entity. It also handles things like "follow terrain" which causes an entity to "stick" to the terrain, or allows it to float above the terrain.
- Movement - For entities which can move, this handles finding paths (on the server) and following paths (on the client).
- Model - On the client, this is used to control the model that the entity is displayed as. Things like animations are controlled by this component as well.
- Mob - This component controls the "AI" of mobs. When not attacking a player, a mob will wander around at random. When attacked by a player, the mob will attempt to fight back.
- Stats - This component manages the entity's stats (health, strength, etc).
- Spawner - This component is used by entities on the server to spawn other entities. It's usually invisible and lets you control things like "spawn X kind of entity, making sure there's never more than N entities active at once).
The world is divided up in 64x64 grids, called "sectors". The sectors are all adjacent to each other, and the client will load up to four sectors to ensure that the world appears seemless as you move around. On the server, sectors aren't really used at runtime, except that the data within them is loaded on demand the first time a client enters that sector.
For example, the first time a player enters a particuar sector, the server will load all the data for that server in to memory (for example, there is an entities.json file which contains definitions for all of the entities which exist in that sector (for example, this will describe environmental objects -- trees, rocks etc, as well as things like spawners and NPCs to populate the world with life.
Long term storage
Long-term storage (or just the "Store") is used to store all the data which needs to persist over server restarts. Things like player stats, inventory, quest status and so on. Every time one of these things is modified in the world, the store is automatically and immediately updated as well.
It's important to note that entities in the game are not nessecarily stored in the persistent store. We generally assume that if the server restarts, then when the sector is reloaded, all the entities we care about will be re-created from the sector data.
As mentioned above, the client maintains a persistent WebSocket connection to the server. If the WebSocket connection drops out, the client pops up a "waiting to reconnect" dialog and tries to re-connect.
The above shows Chrome's debug tool showing the frames sent over the WebServer to the server (and the data received back from it). Data is encoded using JSON (for now, I might switch to a binary format if size/bandwidth becomes a problem).
Basically, on the server there is a player handler which handles decoding the packets from the client. It maintains a reference to the player's entity in the "world", and whenever a packet instructs it to do something, the entity within the world is notified. For example, when the player clicks on another entity, the player handler will issue an "activate" event on the entity you clicked on. Depending on whether you clicked an NPC or a mob, the NPC might send a "show quest page" packet back to the client. Or if it's a mob, an "entity attacked" packet might be sent to all players within visual range.
That's a basic overview of the architecture of the game, such as it is today.
Obviously this is all quite up in the air, and I haven't really arrived here through considered, careful thought, but more through an organic process of evolution.