Architecting an open world RPG

Introduction

Over the past year and a half, I have been architecting an open world RPG, taking after Bethesda games. The fruits of my efforts have manifested into Skelerealms, a work-in-progress framework for these RPGs for Godot 4. The prospect of architecting an open world RPG seems relatively simple on the surface; is it not like any other game where you load a scene with some NPCs in it? Indeed it is, but there is one question that throws a wrench into this whole operation:

How do I make changes persist between scenes?

This is to say, if the player drops an item on the floor, leaves the scene, and then comes back later, how can I make sure the item is still there lying on the floor? How do I make an NPC who is walking from point A to point B keep walking to point B when the player leaves the scene? These questions introduce a lot of complexity into your otherwise relatively simple project. Here’s how I tackled this.

Design Goals

I had a few design goals in mind when creating Skelerealms.

  1. Objects (From now on called “Actors” to discern from object as in object-oriented programming) should not return to their default state when a scene is loaded, allowing continuity in the player’s mind.
  2. The system should be able to run without the Player’s presence.
    • While this seems arbitrary, one of Bethesda’s Creation Engine (Which Skelerealms takes some design hints from)’s great weaknesses is that the Player object has a lot of responsibility within CE games, to the point where the game cannot run without the player. While this is okay for singleplayer games, this became a huge obstacle when Bethesda was designing Fallout 76, which is a multiplayer experience. Making the Player fundamentally no different than any other Actor means that, if designed right, the system would be more flexible, and could allow for multiplayer.
  3. NPCs should be able to move about the world without being within a scene, allowing them to go through doors, and be generally in the place a player would expect.

Out-of-scene persistence - Decoupling the actor and the actor’s representation

That’s quite a mouthful, but in short, the solution I came up with is separating the actual actor, and how it appears in the world. As an example, take, say, a sword. The actual Sword actor, and its data, are one object, and the sword model is another. The two are linked, but this means that the model can be despawned without destroying the Sword actor. If you keep the actor object in a separate database of some kind that will not unload when the scene is unloaded, then the actor and its data will remain, even when the model is unloaded.

In Skelerealms, this idea is implemented as the concept of the Entity and the Puppet:

  • The Entity is the actor, storing position data, item data, etc.
  • The Puppet is the model, ridigbody, and the scripts allowing the player to interact with it.

When the Puppet is in the scene, the rigidbody takes control of the Entity’s position data, meaning that as the Rigidbody falls, rolls around, etcetera, the Entity’s position is updated in kind. The Entity controls whether or not the Puppet is spawned or not. The Entity controls this by determining whether the stored position data is inside the active scene or not, and spawning (or despawning) the puppet in response. This has the nice side effect of automatically despawning the item if the player picks it up, by a process discussed in the next section.

Keeping track of the Entities depends on a number of factors, and is engine-specific. In general, I took a cue from CE, and gave each entity a “RefID” - A unique identifier which allows them to be accessed. They can be both authored and generated (In my case, I author specific characters and items, and anything generated as enemies or loot or something is given a UUID.)

As for the Database aspect, implementing it in Godot was trivial. The Entities are simply kept in a separate tree of nodes from the scene that the player walks around in, and puppets are children of the entities. A Naive approach to accessing Entities would be to simply get_node(ref_id) from an Entity Manager node, but within Godot’s source, this loops through every entity checking the name against the entity’s name - an O(n) operation. Entity lookups happen extremely often, so I opted instead to have the manager also have a Dictionary correlating a RefID to the entity object. This will increate the memory usage a bit, but the performance tradeoff should be well worth it. An early version of what would become Skelerealms was made in Unity, and was a lot more of a pain to implement, because Unity is lame and stupid. The Actors were kept in a very large dictionary inside of a singleton marked not to unload between scenes. The Puppets were GameObjects within the scene. An inventory system works well with this idea - An inventory is not a collection of Actors, but is simply a list of RefIDs, corresponding to the inventory’s contents. Do something else for money, though, you don’t want a usique object for every single Septim or whatever.

Worlds - An abstraction of scenes

How are Actors to keep track of where they are in the game’s universe? A point of (3, 4, 5) in one room is not the same place in the player’s mind as (3, 4, 5) in another room. To solve this, I created a solution reminiscent of CE’s “cell” system (but does not have the same limitations). In short, coordinates have an extra “dimension”, called world, which simply keeps track of what scene that point is in. This new data point is called “World”, and simply corresponds to the name of a scene. That sounds more complicated than it is, but it’s basically you’re normal XYZ coordinate system, but it also contains the scene name. For example, if a book actor is in a scene called “Library”, that book may be at (1, 2, 3.5, "Library").

A sort of “Game Manager” object keeps track of what the active scene is, and is tied to the scene loading system. To check if an actor is in a scene, simply compare its tracked world, and the game manager’s active world. If they match, then the actor is in the scene. There can be other checks, such as how far it is away from the camera, and if an item is in an inventory or not.

The Network - Navigation outside of scenes

The last major system I designed for the fundamental architecture is the idea of a Navigation Network. This is both simple and complex at the same time. The basic idea is to have a node graph of different points in each scene to fill the role os a Navmesh. When a scene is unloaded, it obviously takes its navmesh with it. An NPC can instead fall back on this much lower resolution representation of each scene to move around. It can simply pathfind and lerp between points, since the player will not see this happening and think it looks weird. The complexity comes in when you factor in Worlds. It may be tempting to make one large network for all of the worlds, but it is common in games for different scenes to not line up spacially - and interior can be larger than an exterior. How do we account for this? How do you pathfind across a non-euclidean space? Here’s what I came up with:

  • Each scene/world has its own navigation network.
  • Networks can have “portals” connecting them (corresponding to in-game doors or portals).
  • To optimize a “nearest node” check for pathfinding, each navigation network is compiled into a collection of K-D Trees, one for each world. (You could have one very large K-D tree, with a 4th dimension being the world, and have radius checks that ignore that dimension).
  • Normal A* Pathfinding would not work in most cases, since the XYZ coordinates do not correlate between worlds, and one world cannot be “closer” than another, in the same way width cannot be “closer” than depth. I developed a hybrid solution, wherein it would use Dijkstra’s algorithm when the searching node’s world is not in the target’s world, but if they are in the same world, use A. Or, put simply, use A but turn the heuristics calculation off when the searching world and the target world are different. However, I have not tested this extensively in practice, and I expect it may lead to some odd behavior of NPCs preferring not to go through doors even if it would be shorter. If you have a better solution, let me know.

In practice, an NPC would get its position data from some kind of navmesh navigator when in-scene, but outside of the scene, it would defer navigation to the network system, and lerp between each point.

Conclusion

I hope you found this article helpful in designing your own systems, if Skelerealms (which already does all of this) is not to your liking. There’s much more that goes into developing this sort of system (Savegames, AI, Factions, Combat, etc) but that is beyond the scope of this article. If you have any questions or comments, you can contact me.

Slashscreen

I make games and draw stuff.


2023-11-26