Skip to main content

Welcome to a Grasp lesson

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

Lesson 63

Attaching Colliders to Game Entities

Recommended resources

Introduction

Welcome back! In the last two lessons, you implemented two fundamental collision tests: aabb_intersect and sphere_intersect. These functions are the core mathematical building blocks of our physics engine, allowing us to determine if simple shapes overlap.

However, right now, these tests and the AABB and Sphere structs we created exist in isolation. They aren't connected to the actual objects in our game world. How do we specify that one game object should be treated as a box, while another should be treated as a sphere for collision purposes?

In this lesson, you will bridge that gap. Your goal is to attach collider components (e.g., AABB, Sphere) to our game entities. This will involve formalizing our game object structure using a powerful architectural pattern and designing a flexible way to represent different types of colliders.


From Objects to Components

In programming, we often think in terms of "is-a" relationships, using inheritance. For example, a Player is-a Character, which is-an Entity. This approach can become rigid and complex in game development. What if you want to give an enemy a player-only ability temporarily? The inheritance tree makes this difficult.

Game development often favors a more flexible "has-a" relationship, also known as a component-based architecture. Instead of being a single, monolithic thing, an entity is simply a container for various pieces of data and functionality called components.

  • A player entity has a transform component (position, rotation).
  • It has a physics component (velocity, mass).
  • It has a render component (mesh, texture).
  • And, crucially for us today, it has a collider component.
Game Entity Components Diagram
This diagram illustrates the component-based approach. Each entity (Player, Enemy, etc.) is composed of a unique combination of components. Notice how they all share `Position` and `Collider` components, but differ in others.

For a great conceptual overview of why this compositional approach is so powerful for games, please watch the following video. Since you have experience with C#, this might remind you of the component system used in the Unity game engine. We are essentially building a similar system from the ground up in C.

COMP4300 - Game Programming - Lecture 04 - Intro to ECS in C++ (Entities, Components, Systems)

This video from a game programming course explains the 'why' behind Entity-Component-System (ECS) architecture. It contrasts it with traditional object-oriented design and shows how combining different components allows for flexible creation of diverse game objects.

Watch the segments from 01:27 to 05:01 and from 07:14 to 11:16. The first part introduces ECS, and the second provides excellent examples of defining entities like trees, enemies, and players by composing components.


Designing a Flexible Collider Component

Our first challenge is to design a single Collider component that can represent either an AABB or a Sphere. We need a way to know which type of shape the collider is and to access the corresponding data.

A common and efficient pattern in C for this is to use a struct containing two things:

  1. An enum to act as a type tag.
  2. A union to store the data for the different possible collider types. A union allows several members to share the same memory location, since only one of them will be active at a time.

Let's update our physics_types.h file, which already contains the definitions for AABB and Sphere.

// In physics_types.h




#ifndef PHYSICS_TYPES_H
#define PHYSICS_TYPES_H




#include <cglm/cglm.h>

typedef struct {
    vec3 min;
    vec3 max;
} AABB;

typedef struct {
    vec3 center;
    float radius;
} Sphere;

// 1. Define an enum for our collider types
typedef enum {
    COLLIDER_TYPE_NONE,
    COLLIDER_TYPE_AABB,
    COLLIDER_TYPE_SPHERE
} ColliderType;

// 2. Define the main Collider component struct
typedef struct {
    ColliderType type;
    union {
        AABB aabb;
        Sphere sphere;
    } data;
} Collider;




#endif // PHYSICS_TYPES_H

With this Collider struct, we can now create a collider, set its type to COLLIDER_TYPE_AABB, and store data in collider.data.aabb. Or, we can set the type to COLLIDER_TYPE_SPHERE and use collider.data.sphere. It's a memory-efficient and type-safe way to handle multiple shapes within a single component type.


A Data-Oriented Approach for Entities and Components

Now, how do we attach this Collider component to an entity? To do this in a clean, high-performance way that is well-suited for C, we will adopt a data-oriented design for our entity system.

Instead of an entity being a struct that contains all its components, we will do the opposite:

  • An Entity will simply be an ID (an integer index).
  • All components of the same type will be stored together in large arrays.
  • A central World or Scene struct will hold all these component arrays.
  • We'll use a component mask (a bitfield) for each entity to track which components it has.
Entity-Component-System Memory Layout
This diagram shows the 'Struct of Arrays' memory layout. Instead of an 'Array of Structs' (where each entity struct holds its components), we have a 'Struct of Arrays' (a World struct holding arrays of each component type). This is much more cache-friendly for processing all components of a certain type.

To learn how this is implemented in C, the following article is an excellent guide.

Implementing Component-Entity-Systems

This article, 'Implementing Component-Entity-Systems' from GameDev.net, provides a fantastic, C-focused walkthrough of the data-oriented approach we're adopting.

Read the sections 'Implementation', 'Components', and 'Entities'. Pay close attention to: How an enum is used with bit-shifting (1 << 0, 1 << 1) to create component masks. How the World struct is defined to hold arrays of components. How the createTree function works: it gets an entity ID, sets the data in the component arrays at that ID's index, and updates the entity's mask using the bitwise OR | operator.


Implementation: Building Our Component System

Let's apply these concepts to our project. We will create new files to house our Entity-Component System and update our main loop to use it.

1. Create ecs.h to define components and the World

This file will define our component types and the main World struct that holds all game data. For now, we will have two components: Transform (which we'll formally define now) and Collider.

// ecs.h




#ifndef ECS_H
#define ECS_H




#include <cglm/cglm.h>
#include "physics_types.h"




#define MAX_ENTITIES 1024

// Define which components an entity can have using a bitmask
typedef enum {
    COMPONENT_NONE      = 0,
    COMPONENT_TRANSFORM = 1 << 0, //  1
    COMPONENT_COLLIDER  = 1 << 1  //  2
    // Future components will go here (e.g., COMPONENT_RENDERABLE = 1 << 2)
} Component;

// A Transform component for position, rotation, and scale
typedef struct {
    vec3 position;
    vec3 rotation;
    vec3 scale;
} Transform;

// The World struct holds all our game data in component arrays
typedef struct {
    // A bitmask for each entity that tells us which components it has
    int masks[MAX_ENTITIES];

    // Component arrays
    Transform transforms[MAX_ENTITIES];
    Collider colliders[MAX_ENTITIES];
} World;

// Function prototypes for our ECS
void world_init(World* world);
int world_create_entity(World* world);
void entity_add_transform(World* world, int entity_id, Transform transform);
void entity_add_collider_aabb(World* world, int entity_id, AABB aabb);
void entity_add_collider_sphere(World* world, int entity_id, Sphere sphere);




#endif // ECS_H

2. Create ecs.c to implement the functions

Here we'll write the logic for initializing the world and adding components to entities.

// ecs.c




#include "ecs.h"
#include <string.h> // For memset

/**
 * Initializes the world to a clean state.
 */
void world_init(World* world) {
    // Set all masks to COMPONENT_NONE, effectively clearing all entities
    memset(world->masks, 0, sizeof(world->masks));
}

/**
 * Finds the first available entity ID.
 * @return The entity ID, or -1 if no entities are available.
 */
int world_create_entity(World* world) {
    for (int i = 0; i < MAX_ENTITIES; ++i) {
        if (world->masks[i] == COMPONENT_NONE) {
            return i; // Found an unused entity slot
        }
    }
    return -1; // No more entities
}

/**
 * Adds a Transform component to an entity.
 */
void entity_add_transform(World* world, int entity_id, Transform transform) {
    world->transforms[entity_id] = transform;
    world->masks[entity_id] |= COMPONENT_TRANSFORM; // Update the mask
}

/**
 * Adds an AABB Collider component to an entity.
 */
void entity_add_collider_aabb(World* world, int entity_id, AABB aabb) {
    world->colliders[entity_id].type = COLLIDER_TYPE_AABB;
    world->colliders[entity_id].data.aabb = aabb;
    world->masks[entity_id] |= COMPONENT_COLLIDER; // Update the mask
}

/**
 * Adds a Sphere Collider component to an entity.
 */
void entity_add_collider_sphere(World* world, int entity_id, Sphere sphere) {
    world->colliders[entity_id].type = COLLIDER_TYPE_SPHERE;
    world->colliders[entity_id].data.sphere = sphere;
    world->masks[entity_id] |= COMPONENT_COLLIDER; // Update the mask
}

3. Update main.c to use the new system

Now, let's test our work. We'll replace the old standalone test structs with entities created in our new World.

// In main.c



#include <stdio.h>
#include "ecs.h"
#include "physics.h" // For our intersection functions

int main(void) {
    World world;
    world_init(&world);

    printf("--- Attaching Collider Components Test ---\n");

    // --- Entity 1: A static box ---
    int box_entity = world_create_entity(&world);
    if (box_entity != -1) {
        printf("Created box_entity with ID: %d\n", box_entity);

        // Add a transform component
        Transform box_transform = { 
            .position = {0.0f, 0.0f, 0.0f}, 
            .scale = {1.0f, 1.0f, 1.0f} 
        };
        entity_add_transform(&world, box_entity, box_transform);

        // Add an AABB collider component
        AABB box_aabb = { .min = {-0.5f, -0.5f, -0.5f}, .max = {0.5f, 0.5f, 0.5f} };
        entity_add_collider_aabb(&world, box_entity, box_aabb);
    }

    // --- Entity 2: A player-like sphere ---
    int player_entity = world_create_entity(&world);
    if (player_entity != -1) {
        printf("Created player_entity with ID: %d\n", player_entity);

        // Add a transform component
        Transform player_transform = { 
            .position = {1.5f, 0.0f, 0.0f}, 
            .scale = {1.0f, 1.0f, 1.0f} 
        };
        entity_add_transform(&world, player_entity, player_transform);
        
        // Add a Sphere collider component
        Sphere player_sphere = { .center = {0.0f, 0.0f, 0.0f}, .radius = 0.5f };
        entity_add_collider_sphere(&world, player_entity, player_sphere);
    }
    
    // Now you can access the component data via the world arrays
    printf("Box entity's collider type: %d (1=AABB, 2=Sphere)\n", world.colliders[box_entity].type);
    printf("Player entity's collider type: %d (1=AABB, 2=Sphere)\n", world.colliders[player_entity].type);

    return 0;
}

Compile and run your project. You should see output confirming that two entities were created and that their collider types were set correctly. We have successfully attached different collider types to different entities!


Conclusion

Fantastic work today! This was a conceptually heavy lesson, but it establishes a crucial architectural foundation for the rest of your game engine. You have moved from having disconnected functions to having a robust, data-oriented system for defining game objects.

Key Takeaways:

  • Component-Based Architecture favors composition ("has-a") over inheritance ("is-a"), leading to more flexible and modular game objects.
  • In C, components are often simple structs that hold pure data.
  • An enum and union pair is an effective C pattern for creating a component that can represent multiple types of data, like our Collider.
  • A data-oriented ECS uses entity IDs, arrays of components (Struct of Arrays), and bitmasks to efficiently manage game objects and their data.
  • We can now add a Collider component, of either AABB or Sphere type, to any entity in our World.

Preview of the Next Lesson:
Our entities now have colliders, but we're not doing anything with them yet. In the next lesson, we will complete the loop by creating our first "System". We will learn how to implement a system that checks for collisions between all relevant entities each frame, iterating through our entities, checking their component masks, and calling the appropriate intersection test.