This article is part of a series.

Part 1: An OOP Overview

Part 2: Framework Concepts

Part 3: Engine Comparisons

Edit 1: Clarified use of “entity” and “component” terms to direct away from the traditional ECS meanings. Part 1 elaborates on differences.

Introduction

While the last article grounded us in basic OOP concepts, this one will dive into common features of frameworks, i.e. collections of object relationships. Game frameworks do not require these ideas in order to be a framework by any means. Instead, these ideas will try to paint a picture of what one can expect from the game frameworks of today’s most popular game engines.

Moving forward with this article, note that references to the terms “entity” or “component” are references not to the aspects of an ECS paradigm. Rather, a traditionally OOP Objects’ features are split up into essential elements called “components”. Anything in the world that has components is an “entity”.

Structure

As mentioned in the previous article, some engines use component systems in their frameworks. These are frameworks that rely heavily on various composition techniques rather than inheritance.

Because component systems are becoming more prevalent, we will be assuming this to be the case in our overview of game framework concepts. If it doesn’t have a component system, then chances are these concepts are still used, but the exact terminology and how they apply will be slightly different.

Pretty much all engines have users create some “space” in which to create content. While the name used for these spaces may vary, we’ll simply say “level”. Users create a level with an environment paired with a list of entities. Users can edit the environment or place entities to customize their game content.

But, these game engines don’t need any entities in a level to run it. An empty level will execute just as cleanly as a populated one. You usually need a Camera entity to see or hear anything, but the editors will still run the level without it.

The entities can own other entities as children (tree structures), so users can organize things. Each entity also supports ownership of components, but the components don’t own things. The entity maintains all ownership as the user organizes its internal components.

Serialization

But what if a user wanted to define an entity with components that they will use many times? What if they could describe an entity with components, save it to a file, and then load it up whenever they wanted?

Well, first they’d need some automated way of converting the entity and all its components into a file format (usually binary, i.e. 0s and 1s). And thus the engines added Serialization for their entities. With a simple operation, users can take a built entity and save it for later production.

Devs do this by storing information about properties, usually its name and value. They first specify some means of formatting that information (text data, binary data?) and then write it directly to a file. They do this for the entity, all of its components, and then each of its sub-entities and their components.

When devs need to create the entity hierarchy again, they load up the file. They determine how each entity is related, which components they have, and which properties should have what values. Then, they create those entities and components with the associated properties, effectively reproducing the stored entity.

In no particular framework, here’s an example of how this might be done in C++.

int main() { // We will record this integer value under the name "count" int count = 250; File f; // Create and open a file called "stats.txt" bool is_open = f.open("stats.txt", File::WRITE); if (!is_open) { return ERR_COULD_NOT_OPEN_FILE; //report failure } f.write_str("count"); // record the name to the File's buffer f.write_int(count); // followed by the value. //the file will record how many bytes the integer takes up as well f.save(); // the data from the buffer has now been written to disk f.seek(0); // move back to the beginning of the file String name = f.read_str(); //fetch the name of the data if (name == "count") { //in this case... int count = f.read_int(); //load the value into our variable } //we have now de-serialized the "count" variable f.close(); //terminate the file connection to release OS resources }

Instancing

Users can now start creating many of this entity all throughout their project. But what if they need to make a change to each one? They’d need some way of editing one and having that change applied to all related entities.

Well, developers found a solution for that too. When users create the entity, the entity remembers which file the user deserialized it from. If all the entities use the file as a guide for how to exist, then editing the file can change all the entities at once.

Creating these entities from the serialized type is called “instancing”. Here’s another example of how this works in C++, using no specific framework.

int main() { //setup types and data Entity e1; Entity e2; CountComponent cc; cc.count = 250; //give each Entity a copy of the component e1.add_component(CountComponent(cc)); e2.add_component(CountComponent(cc)); //create a hierarchy of entities e1.add_child(e2); //save the hierarchy to a file. Serializer::save(e1, "entity.dat"); //load the file SerializedEntity se; se.load("entity.dat"); //Create a few instances. //Each has a copy of e1 and e2, each with a CountComponent. Entity e3 = se.instance(); //remembers "entity.dat" created it Entity e4 = se.instance(); //same //make sure one of them is overridding the default value e4.get_component<CountComponent>().count = 400; //update the original data e1.get_component<CountComponent>().count = 300; //re-serialize it Serializer::save(e1, "entity.dat"); //de-serialize the data again. //another possibility is that the framework detects that "entity.dat" has changed and updates se automatically. se.load("entity.dat"); //propagate the data to all instances. //Will create an instance of the data and then //diff all components and fields against each instance. //If the value is not overridden, go ahead and reset it. //Again, the framework may automatically do this. se.propagate(); //via Observer pattern, explained later. //prints reloaded 300 print(e3.get_component().count); //prints individually overridden 400 print(e4.get_component().count); e4.reset(); //framework may provide a means of resetting. //prints the reset value of 300 print(e4.get_component().count); }

So now we can make edits to one thing and have those changes propagate to other instances. Sounds a little like inheritance, right? Well, it is an extension of the inheritance concept, but this time, it’s a little different.

Prototypes

Developers have a couple ways to design inheritance. They can build objects from an idea, i.e. “class” (which we covered earlier), or they can create an object and have the instanced copy look to it for guidance on how to exist. This difference is one of class vs. prototypal inheritance.

The key distinction is in what the objects look to for guidance. If a user requests a property from an object, the object has to check whether it has the property.

Class-based objects don’t define properties. Instead, they check their class and its inherited classes (which again, are just ideas). If any of those classes have the property, then the object will too. Users usually define classes in the beginning and can’t edit them as the program executes.

A Prototype-based object can define its own properties. But, it also has an inherited prototype, another object. If the current object can’t find a requested property, it asks its prototype. If the prototype object has that property, then so does the current one. Continued failures keep moving up the prototype chain until the base prototype (Delegation).

The trick here is that prototypes aren’t ideas. They are fully-fledged objects. If the prototypes gain new properties during execution, all their derived types do too. What’s more, a prototype can change at run-time, meaning inheritance is much more fluid. An object may be a drop-in replacement for one type one moment and another type the next moment.

I won’t be doing a code example of Prototype logic. Ample examples of that can be found everywhere since JavaScript, an extremely popular language, is built upon prototypal inheritance.

Networking

Devs often need to send data and execution instructions over a network. Most frameworks give people low-level controls for reading and writing network data. Writing involves sending network data to particular IP addresses and ports. Reading likewise involves listening for network data on particular ports.

Developers have several options when it comes to how to use or implement networking.

They can send network data on reliable, slow connections with TCP or on unreliable, fast connections with UDP ( TCP vs UDP ).

TCP vs UDP They can visualize the network data as individual messages (packets) or as a stream of data (WebSockets) ( Packets vs. WebSockets ).

Packets vs. WebSockets They can have each application fully aware of the game state (peer-to-peer) or choose a single entity to be the authority on the game state (client-server) ( P2P vs. client-server ) . Note that when the authority is not one of the players, devs call it a “dedicated server.”

Managing these relationships can be incredibly complex. Low-level controls like that are cumbersome for simpler, more common behaviors, such as…

Updating a local variable and synchronizing that update on the remote computers.

Calling a function locally and replicating that call on the remote computers.

Notifying the game state of a change, but not needing to notify other players, e.g. pausing.

Tasks such as these should be more accessible to developers. To simplify things, these frameworks can also define a high-level networking API (HLAPI). It generally creates utilities for client-server designs:

It assists in creating, managing, and destroying player connections.

It verifies which connected player is the authority.

It can test whether the currently executing context is the authority (to create client- or server-specific methods).

It enables users to mark which properties/methods it should replicate over the network automatically , to whom, and how . A game engine’s editor usually has some setting in its GUI that enables one to trigger different aspects of the HLAPI in regards to particular variables or methods. As you can tell, networking is a deep topic with many associated technologies. As such, a concrete example of its usage is difficult. But, you might see something like this at the low level… int main() { Socket s; int count = 250; String ip_address = "1.1.1.1"; int port = 9000; //send the "count" value to the machine at IP address "ip_address" //and send the data to the "port" port on that machine. s.send_int(ip_address, port, count); //Now listen on the same port for a confirmation of arrival from //the other machine. while (s.listen(9000)) { //maybe s.listen() has logic for timing out & returning false } } …and something like this at the high level (but still in engine code, sort of – this is kind of a bad example, but it’s shorter and requires little explanation to understand). int main() { Integer count; Game game; //declares that "count" is a variable in the game w/ value 5 //Also declares that it should synchronize the value to all players //All players now know that "count" exists on each machine. game.declare_int("count", &count, 5, REPLICATE_MODE::SYNC); //The Integer class is handling logic to propagate the new //value to other machines automatically through Game's HLNAPI count.value = 5; } Events So, users want to maintain a loose coupling, but they also want to connect behaviors. By separation of concerns, an owned object should never know the details of its parent or siblings. But what if the child does something, and the parent or siblings need to react? What’s the best way for it to notify the others? A poor implementation might be for it to directly access the parent, and through it, the siblings, to call a method on them. That creates a tight coupling though as the notifier now has to know the type that it notifies. It also is forming a dependency whereby it can only exist in a parent that also has the required sibling. To prevent this, devs usually make an event system available to the framework’s users. The parent already is aware of both siblings’ existence and types. It also knows that the sibling will need to react. Instead of the notifier taking matters into its own hands, the parent simply tells the sibling to listen for the event on the notifier. The notifier can then emit the event. Because the sibling was listening for it, it will react. Neither the notifier nor the sibling knew each other. The parent orchestrates everything from behind-the-scenes. Devs call this design pattern the Observer pattern. Almost every gaming framework has an event system of this nature. Some even have more than one for different purposes (input, collision detection, user-defined, etc.). JavaScript has several examples of the Observer pattern since it is also built upon it, via EventListeners. This pattern can be found in virtually every language. We’ll cover its usage in modern game engines and their languages later. Tags Now users may start creating a huge number of entities in their games. What if they just need a certain group of them? What if they just need one? It would be inefficient to cycle through all existing entities and compare their IDs to a list. Instead, it might be a good idea to set up a cache of entities that fit into particular categories. Looking through a list of 1 or 10 entities is a lot faster than looking through 1,000 or a 100,000. Well, devs decided to allow users to create these caches at will. To access one, they need to give it a name and add entities to the cache. Users can then perform lookups for these names, a.k.a. “tags” to find entities or they can check which tags an entity has. Here’s another simple example in non-specific C++ code. int main() { Game game; PlayerEntity pe; pe.add_tag("player"); game.add_entity(pe); //add the player //add 1000 miscellaneous entities Entity e; for (int i = 0; i < 1000; i++) { game.add_entity(Entity(e)); } EntityArray arr = game.get_entities_by_tag("player"); print(arr.size()); //prints 1 TagArray arr2 = pe.get_tags(); print(arr2); //prints ["player"] } Conclusion While many more features are also shared between engines, these are the highlighted ones here. You should by now have an idea of what to expect in popular game engines nowadays, as well as why those features are available in the first place. Again, we’re only touching the surface here, and we could never go into enough detail on even one of these topics (or sub-topics) in a single article. Feel free to look further into these concepts (especially other “design patterns” like the Observer concept above). There’s a nearly endless amount of material ripe for learning. If you feel like you have a general grasp of these concepts, then you should be ready for us to talk about how some of today’s engines actually incorporate them. The next article will explain how Unity, Unreal, and Godot enable these designs for their users. If you liked the article, or if something was confusing or off-sounding, please let me know! I’d love to hear from you in the comments. I do my best to ensure each article is updated as conversations progress. Cheers!