Advanced Code Architecture for Core, Part 2: Entity-Component-System

GUIDE TITLE: Advanced Code Architecture for Core, Part 2: Entity-Component-System
ESTIMATED COMPLETION TIME: 10 minutes
CORE VERSION: Any

SUGGESTED PREREQUISITES: Intermediate-level coding

TUTORIAL SUMMARY:
Part 2 of a series of articles on designing a better code architecture that solves the problems with the way code is typically structured in Core.

EXPECT TO LEARN:

  • What ECS architecture is
  • Why ECS architecture is beneficial to code in Core
  • How ECS architecture can be implemented in Core
  • How ECS architecture fits together with MVC architecture in Core

END PRODUCT VISUALS:

  • (Imagine beautiful code here.)

TUTORIAL:


Part 1 examined Model-View-Controller (MVC) architecture as the first step to a better architecture for Core games. The key insight of MVC architecture was in thinking of game objects in a scene as the equivalent of UI objects—which meant game logic should be moved out of scripts attached to game objects, into the Model layer instead. However, this left the question of how all of that game logic within the Model layer should then be structured. Naturally, answering that question is the next step.

Entity-Component-System

Entity-Component-System (ECS) is an architectural pattern that has gained a lot of traction in game development. It can be considered an evolution of—and is often confused with—an "Entity-Component" architectural pattern, which lacks the essential "System" aspect (like in the popular Unity game engine). Under ECS architecture, an Entity is an ID representing a game object; a Component is a slice of data associated with an Entity; and a System provides behavior to all Components of the types relevant to the System.

One of the main attractions of ECS architecture is the performance gains from facilitating data-oriented design, when iterating through Components stored in a contiguous span of memory. However, ECS architecture implemented in Lua won't be able to realize these performance benefits, due to limitations of the language. Nonetheless, the underlying concepts of ECS architecture still play a crucial role in establishing a better architecture for Core games, as they have done for non-Core games alike.

Entity

An Entity is simply an ID that represents a conceptual game object. Here, a "game object" isn't an actual object, but rather the composite of: data held by Components associated with the Entity ID, and behavior bestowed by Systems that manipulate those Components.

Because code in Core games often needs to integrate with frameworks and code from other creators, Entity ID's should be System-specific, to support modular design. Each System should have its own registry of relevant game objects, and assign System-specific ID's, instead of depending on a global registry for Entity ID's like in typical ECS architecture.

Depending on the System, Entity ID's may simply be CoreObject references. But, for game objects that don't have a CoreObject representation, or have a CoreObject representation that's spawned into Static/Local Context (which might be different references between its client-side and server-side copies), integer-based Entity ID's must be used instead.

Component

Components are where all actual game state for game objects is stored. Each Component is associated with an Entity ID, and represents one aspect of a game object. A game object will typically need multiple Components, to cover all of its different aspects.

In typical ECS architecture, Components exist at a global level, and can be shared by multiple Systems. However, like Entity ID's, Components in Core games should be System-specific, to support modular design. Each System should have its own registry, which maps an Entity ID to a Component containing System-specific game state for their game object.

Components should be implemented as object-like tables, which may (optionally) have methods for updating their data. These methods shouldn't contain any game logic, but may perform basic validation to ensure internal consistency of the Component's data. Any validation that involves game logic should be performed by Systems, before handing off to a Component.

System

Systems are the most important facet of ECS architecture in Core games. In addition to Entity ID's and Components being System-specific, almost all game logic should be segmented into Systems, with each System responsible for a single feature or game mechanic.

A System isn't a single script or class, but rather a collection of cohesive scripts, which implements a feature. At a minimum, a System comprises a script asset in Project Content, containing the System's Components and game logic; and a script object in the Hierarchy, serving as a centralized entry point by receiving events, or running a repeating Task.

Each System maintains a registry of its own Entity ID's and Components for relevant game objects, and houses the game logic that operates on those Components. Game objects must be registered with Systems, so that the corresponding Components can be created for each of their aspects.

Functions in Systems should be either query functions, which retrieve Component data or other game state; or command functions, which correspond to actions that can be performed on game objects, and can change Component data. Changes to Component data should always be through command functions in the System the Component belongs to.

MVC + ECS

By using Entity ID's as an indirect reference for game objects, game state can be moved into Components associated with the Entity ID, and game logic can be moved into Systems that manage those Components, all within the Model layer. Scripts that serve as the entry point for Systems then naturally fit into the Controller layer; while scripts for registering CoreObjects with Systems stay within the View layer.

While MVC architecture helps segment code vertically into layers (like layers of lasagna), ECS architecture helps segment code horizontally into slices (like slices of lasagna), where each System is a slice. The combination of MVC and ECS architectures is the key to preventing code in Core games from becoming a jumbled mess of "spaghetti code", and opens the door to further, pivotal improvements to architecture, which will be discussed in Part 3.

Too Long; Didn't Read

  • MVC architecture established that game logic should be moved out of game objects, into the Model layer instead; Entity-Component-System (ECS) architecture answers the question of how all of that game logic should then be structured.
  • An Entity is a System-specific ID that represents a game object; Entity ID's should typically be CoreObject references or integers.
  • A Component is a System-specific data structure where all game state for one aspect of a game object should be stored; Components should be implemented as object-like tables, and shouldn't contain any game logic.
  • A System is where all game logic for a single feature or game mechanic should belong; each System should have a script in Project Content that contains its Components and game logic, and a script in the Hierarchy that serves as a centralized entry point.
  • Components and game logic of Systems belong in the Model layer; scripts that serve as the entry point for Systems belong in the Controller layer; and scripts for registering CoreObjects with Systems belong in the View layer.

What's Next

Part 3 will examine Inversion of Control—the most powerful architectural concept to be introduced for code in Core games.

Supplement: Domain-Driven Design

For those who are familiar with coding concepts more common in non-game apps, the particular implementation of ECS architecture described in Part 2 was originally inspired by Domain-Driven Design (DDD). In comparison: an ECS game object is a DDD Entity; an ECS Entity is a DDD Identity; an ECS Component is a DDD Domain Model; and an ECS System is a DDD Service or Aggregate Root.