Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 47

Crafting Your First Game Object: The Entity Struct

Recommended resources

Introduction

Hello! In our last lesson, we built a professional-grade game loop that separates a fixed-timestep update from a variable-timestep render. We used simple previousState and currentState structs to hold our object's position for interpolation, ensuring our simulation is stable and rendering is smooth.

This was a great start, but a struct holding only a vec3 position is quite limited. A game world is filled with diverse objects: the player, enemies, bullets, inanimate scenery, power-ups, and more. A player might have health and velocity, a static wall has a position but doesn't move, and a bullet might have a high velocity but no health. How can we represent all these different objects without creating dozens of slightly different structs, or one giant, wasteful struct with every possible property?

Today's learning outcome is to design a basic Entity struct to represent any object in the game world. We will move beyond a single, monolithic object definition and explore a powerful architectural pattern based on composition. This will allow us to build any type of game object by combining small, reusable pieces of data, much like building with LEGO bricks.


1. A Quick Refresher on C structs

Before we design our entity system, let's quickly solidify our understanding of C structs, as they are the fundamental tool we'll be using. Since you're new to C, this video will be an excellent refresher on the syntax and some powerful features like typedef and how structs handle pointers.

struct Basics | C Programming Tutorial

This video from Portfolio Courses covers the essentials of using structs in C. Pay close attention to how typedef is used to create cleaner code and the important caveat about what happens when a struct containing a pointer is copied.

Please watch the following segments: 00:00 - 04:55: Covers the basic syntax for defining a struct, creating instances, and accessing members with the . operator. 06:44 - 08:20: Explains how to use typedef to create an alias for a struct type, which is a very common and recommended practice in C. 13:23 - 14:22: Shows what happens when you assign one struct to another (p1 = p2). This is a direct memory copy. 17:49 - 24:38: This is the most critical part. It explains the behavior when a struct contains a pointer. The assignment copies the pointer's address, not the data it points to. Understanding this is crucial for avoiding hard-to-find bugs.


2. The Problem: Inheritance vs. Composition

With your background in C# and Python, you're likely familiar with object-oriented programming and inheritance, where you create a "base" class and then more specialized "child" classes that inherit its properties. While powerful, this can become rigid and complex in game development. What if you want a "Flying, Shooting Enemy"? Do you inherit from FlyingEnemy and ShootingEnemy? Many languages, including C, don't support multiple inheritance, leading to tangled hierarchies.

A more flexible approach is composition. Instead of an object being a single, rigid thing, it has a collection of properties.

Inheritance vs. Component-Based Game Object Design
This diagram shows the core idea. On the left, the inheritance model struggles with a 'Shooting Enemy'. On the right, the component model easily creates one by simply giving a 'Game Object' a 'Shoot' component and a 'Move' component.

This brings us to the Entity-Component-System (ECS) pattern, a cornerstone of modern game engine design.

  • Entity: A general-purpose object. In its purest form, it's just a unique ID number. It doesn't have any data or behavior itself.
  • Component: A small, data-only struct that holds a specific piece of information. For example, a TransformComponent holds position and rotation, while a HealthComponent holds current and max health.
  • System: The logic that operates on entities that have a certain set of components. For example, a PhysicsSystem would look for all entities that have both a TransformComponent and a PhysicsComponent and update their positions.

By mixing and matching components, we can create any object imaginable.

Entity-Component System Illustration
Here, you can see how different entities are just different combinations of components. A 'Base' is just a visual and something that can take damage. An 'Enemy' is a visual that can move. A 'Shooting Enemy' is a visual that can move and fire.

The following video gives a great conceptual overview of ECS.

Entity Component System (ECS) - Definition and Simple Implementation

This short video by Dylan Falconer introduces the ECS pattern and its benefits. It clearly defines entities, components, and systems and explains why this pattern promotes flexibility.

Watch the first five and a half minutes (00:00 - 05:32). Focus on the definitions of Entity, Component, and System, and the core strengths of this approach: flexibility and composability.


3. Designing Our Entity and Component "World"

So, how do we implement this in C? The learning outcome is to "design a basic Entity struct". As we've just learned, in a pure ECS, an entity is just an ID. The "design" is therefore not about a single struct, but about the collection of data structures we use to manage all our entities and their components. We'll call this our World.

Our design will be as follows:

  1. An Entity will be a simple typedef for an integer ID.
  2. Components will be simple C structs containing only data.
  3. Our World struct will contain arrays for our component data. For every entity, world.transforms[entity_id] will be its transform component, and so on.
  4. We'll use a bitmask to track which components each entity has. This is a very efficient, C-style approach.

Let's start defining this.

Step 1: Define Entity ID and Component Types

First, let's create a header file, say ecs.h, for our definitions.

// ecs.h




#ifndef ECS_H
#define ECS_H




#define MAX_ENTITIES 1024

// An entity is just an ID.
typedef unsigned int Entity;

// A bitmask of component types.
// We use powers of 2 to allow for bitwise operations.
typedef enum {
    COMPONENT_NONE      = 0,
    COMPONENT_TRANSFORM = 1 << 0,  //   1
    COMPONENT_PHYSICS   = 1 << 1,  //   2
    COMPONENT_RENDER    = 1 << 2,  //   4
    // Add new component types here
} ComponentType;




#endif // ECS_H

By using powers of two for our enum, we can represent any combination of components with a single integer. For example, an entity with a mask of 5 (binary 101) would have COMPONENT_TRANSFORM and COMPONENT_RENDER.

Step 2: Define The World Struct

The World struct will be the central manager of our game state. For this basic design, we'll use fixed-size arrays.

// ecs.h (continued)

// First, forward-declare our component structs
// (we will define them properly in the next lesson)
struct TransformComponent;
struct PhysicsComponent;
struct RenderComponent;

typedef struct {
    // The number of entities currently in use.
    unsigned int count;

    // A mask for each entity that tells us which components it has.
    // The index in this array corresponds to the entity ID.
    // e.g., masks[5] is the component mask for entity 5.
    int masks[MAX_ENTITIES];
    
    // Arrays of component data.
    // The index corresponds to the entity ID.
    struct TransformComponent* transforms[MAX_ENTITIES];
    struct PhysicsComponent* physics_bodies[MAX_ENTITIES];
    struct RenderComponent* renderables[MAX_ENTITIES];

} World;

// We also need functions to manage the world.
World* World_Create();
void World_Destroy(World* world);
Entity World_CreateEntity(World* world);
void World_AddComponent(World* world, Entity entity, ComponentType component_type);

Important Design Choice:
Notice that the World struct holds arrays of pointers to components, not the components themselves. This means if an entity doesn't have a certain component, its entry in the array will simply be NULL, saving memory. We will need to malloc components individually when we add them to an entity. This is a trade-off: it's slightly slower than having contiguous arrays of components, but much more memory-efficient for a sparse world where not all entities have all components.

Step 3: Implement World Management Functions

Now let's create ecs.c to implement these functions.

// ecs.c




#include <stdio.h>
#include <stdlib.h>
#include "ecs.h"
#include "components.h" // We'll create this file in the next lesson

World* World_Create() {
    World* world = malloc(sizeof(World));
    world->count = 0;
    // Initialize all masks and pointers to 0/NULL
    for (int i = 0; i < MAX_ENTITIES; ++i) {
        world->masks[i] = COMPONENT_NONE;
        world->transforms[i] = NULL;
        world->physics_bodies[i] = NULL;
        world->renderables[i] = NULL;
    }
    return world;
}

void World_Destroy(World* world) {
    // Free any allocated components
    for (int i = 0; i < world->count; ++i) {
        if (world->transforms[i]) free(world->transforms[i]);
        if (world->physics_bodies[i]) free(world->physics_bodies[i]);
        if (world->renderables[i]) free(world->renderables[i]);
    }
    free(world);
}

Entity World_CreateEntity(World* world) {
    if (world->count >= MAX_ENTITIES) {
        // In a real engine, you might resize your arrays here.
        // For now, we'll just error out.
        fprintf(stderr, "Error: Maximum entities reached.\n");
        return -1; // Return an invalid ID
    }
    
    // Find the next available ID
    Entity new_entity_id = world->count;
    world->count++;

    // Ensure its mask is clean
    world->masks[new_entity_id] = COMPONENT_NONE;
    
    return new_entity_id;
}

// A simple example for adding a transform component.
// You would have similar functions for other components.
void World_AddTransformComponent(World* world, Entity entity) {
    // Check if the entity ID is valid
    if (entity >= world->count) return;

    // Use bitwise OR to add the component to the mask
    world->masks[entity] |= COMPONENT_TRANSFORM;
    
    // Allocate memory for the component data
    world->transforms[entity] = malloc(sizeof(struct TransformComponent));
    
    // Initialize with default values (example)
    // glm_vec3_zero(world->transforms[entity]->position);
    // ...
}

This design fulfills our learning outcome. We have a way to represent any object (Entity), and its structure is defined by the components we add to it, which are managed by our World.


Conclusion

Today, you've taken a massive step in your engine's architecture. Instead of thinking about objects in rigid, hierarchical terms, you've designed a flexible, data-driven system based on composition.

Key Takeaways:

  • Composition over Inheritance: Building objects by giving them components is more flexible for game development than using rigid inheritance trees.
  • Entities are just IDs: In a component-based architecture, an entity is little more than a unique number used to index into data arrays.
  • Components are data: They are simple structs that hold information for a single aspect of an entity (e.g., position, physics, health).
  • A World or Scene manages everything: A central struct holds the component data and the masks that link entities to their components.
  • Bitmasks are an efficient, C-style way to track which components an entity has.

Preview of the Next Lesson:
We've designed the scaffolding for our entity-component system. In the next lesson, we will put flesh on these bones by focusing on the learning outcome: Define core data components like Transform (position, rotation, scale) and PhysicsBody. We will create the actual struct definitions for these components and integrate them into the World we designed today.