Skip to main content

Welcome to a Grasp lesson

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

Lesson 48

Understanding Core Data Components: Transform & PhysicsBody

Recommended resources

Introduction

Hello again! In our last lesson, we designed the architectural backbone of our game engine: a basic Entity-Component-System. We established that an Entity is just an ID and created a World structure to manage all our game objects. However, the actual Component structs were left as forward-declarations.

Today, we'll give those declarations substance. Our learning outcome is to define core data components like Transform (position, rotation, scale) and PhysicsBody. These two components are foundational. Nearly every object in your game world will need a Transform to define its place in 3D space, and anything that moves or interacts physically will need a PhysicsBody.

By the end of this lesson, you will have created the C struct definitions for these components and integrated them into the World structure we built previously, turning our abstract design into concrete, usable code.

3D Geometric Shapes and Coordinate System with Physics Annotations
Objects in a 3D world are defined by their position, orientation, and physical properties. We'll capture this information in our components.

1. The TransformComponent

The Transform is arguably the most fundamental component in a 3D engine. It answers the question: "Where is this object, how is it oriented, and how big is it?" It typically consists of three properties:

  • Position: Its location in 3D space (an X, Y, Z coordinate).
  • Rotation: Its orientation around the X, Y, and Z axes.
  • Scale: Its size along the X, Y, and Z axes.

This panel from the Unity game engine is a perfect visual representation of the data we need to store.

Unity Transform Component Panel
A typical Transform component in a game engine, showing Position, Rotation, and Scale, each with X, Y, and Z values.

Design Decision: Storing Transform Data

A key design choice is how to store this information. While a 4x4 matrix can represent all three transformations at once, storing them as separate position, rotation, and scale vectors is far more flexible and intuitive. This approach makes it easier to:

  • Individually manipulate properties (e.g., "move the object up by 1 unit").
  • Avoid mathematical complexities and precision issues with storing large rotation values directly in a matrix.
  • Save and load object data in a human-readable format.

We can always compute the final 4x4 transformation matrix from these three properties whenever the rendering or physics systems need it.

The following video discusses this exact topic. Although it uses C++, the reasoning is universal and provides excellent context for our C implementation.

Transform Component UI | Game Engine series

In this clip from his "Game Engine series", The Cherno explains why storing position, rotation, and scale separately is preferable to using a single matrix. He then modifies his TransformComponent to reflect this.

Watch the segments from 03:27 to 11:40. Focus on: The explanation for why storing transform data as a single matrix has limitations. How the TransformComponent is changed to store translation, rotation, and scale as separate vec3 vectors. The logic for constructing a final transformation matrix from these separate components.

Defining the TransformComponent in C

Inspired by this, let's create a new header file, components.h, and define our TransformComponent. We will use the cglm library for our vector types (vec3), which is a common choice for C-based graphics projects.

// components.h




#ifndef COMPONENTS_H
#define COMPONENTS_H




#include <cglm/cglm.h>

// Defines the object's position, rotation (Euler angles), and scale in 3D space.
typedef struct {
    vec3 position;
    vec3 rotation; // Stored in radians
    vec3 scale;
} TransformComponent;




#endif // COMPONENTS_H

A quick but important note on rotation: we will store rotation values as Euler angles in radians, as this is what most math libraries (including cglm) expect for trigonometric functions. If we build a UI later, we can easily convert to and from degrees for display, a practice also mentioned in the video you just watched (timestamp 35:27).


2. The PhysicsBodyComponent

While the TransformComponent gives an object a state (its current position), the PhysicsBodyComponent gives it the potential for dynamics. It holds the properties needed for a physics simulation. A simple but effective physics body needs:

  • Velocity: The rate of change of position (a vector).
  • Acceleration: The rate of change of velocity (a vector).
  • Mass: A scalar value representing its inertia.

Later, we can add properties like friction, restitution (bounciness), and a vector to accumulate forces, but this is a solid start.

This next video clip shows a C implementation of a body struct that is very similar to what we need for our PhysicsBodyComponent.

Physics: Integration | C Game + Engine From Scratch 05

In this brief clip from his "C Game + Engine From Scratch" series, Dylan Falconer defines the structs for his physics system.

Watch from 03:24 to 04:11. Pay attention to the definition of the body struct. It contains velocity and acceleration, which are the core elements we need for our PhysicsBodyComponent.

Defining the PhysicsBodyComponent in C

Let's add our PhysicsBodyComponent definition to components.h. We'll also add a force vector to accumulate forces applied to the object each frame.

// components.h (continued)

// Holds data for physics simulation (e.g., movement, gravity).
typedef struct {
    vec3 velocity;
    vec3 acceleration;
    vec3 force; // Sum of forces applied in a single frame
    float mass;
} PhysicsBodyComponent;

// --- Don't forget the closing #endif from before ---

In our physics system, a typical update loop for an object with this component would be:

  1. Apply forces (like gravity) by adding to the force vector.
  2. Update acceleration using Newton's second law: .
  3. Update velocity: .
  4. Update the object's position (in its TransformComponent): .
  5. Clear the force vector to (0, 0, 0) for the next frame.

If you're interested in a more advanced design, the article "Designing a physics engine" by Winter is an excellent resource. It shows how this basic concept can evolve into a Rigidbody struct with properties for friction and bounciness.


3. Integrating Components into the World

Now that we have components.h, let's update the ecs.h and ecs.c files from the previous lesson to make our system functional.

Step 1: Update ecs.h

The forward-declarations are no longer needed. We can now include components.h and use the concrete types.

// ecs.h (Updated)




#ifndef ECS_H
#define ECS_H




#include "components.h" // Include our new component definitions




#define MAX_ENTITIES 1024

typedef unsigned int Entity;

typedef enum {
    COMPONENT_NONE      = 0,
    COMPONENT_TRANSFORM = 1 << 0,
    COMPONENT_PHYSICS   = 1 << 1,
    // Note: We'll leave the render component for a future lesson
} ComponentType;

typedef struct {
    unsigned int count;
    int masks[MAX_ENTITIES];
    
    // Now these are concrete types!
    TransformComponent* transforms[MAX_ENTITIES];
    PhysicsBodyComponent* physics_bodies[MAX_ENTITIES];

} World;

World* World_Create();
void World_Destroy(World* world);
Entity World_CreateEntity(World* world);

// We'll create specific functions for adding each component type
void World_AddTransformComponent(World* world, Entity entity);
void World_AddPhysicsBodyComponent(World* world, Entity entity);

Step 2: Update ecs.c

We can now implement the World_Add... functions to allocate memory and initialize our new components with sensible defaults.

// ecs.c (Updated)




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

// World_Create() remains the same, just ensure physics_bodies is NULL'd out.
World* World_Create() {
    World* world = malloc(sizeof(World));
    world->count = 0;
    for (int i = 0; i < MAX_ENTITIES; ++i) {
        world->masks[i] = COMPONENT_NONE;
        world->transforms[i] = NULL;
        world->physics_bodies[i] = NULL;
    }
    return world;
}

// World_Destroy() must also be updated to free the new component type.
void World_Destroy(World* world) {
    for (unsigned 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]);
    }
    free(world);
}

// World_CreateEntity() remains the same.
Entity World_CreateEntity(World* world) {
    if (world->count >= MAX_ENTITIES) {
        fprintf(stderr, "Error: Maximum entities reached.\n");
        return (Entity)-1;
    }
    Entity new_entity_id = world->count++;
    world->masks[new_entity_id] = COMPONENT_NONE;
    return new_entity_id;
}

// --- NEW IMPLEMENTATIONS ---

void World_AddTransformComponent(World* world, Entity entity) {
    if (entity >= world->count) return;

    world->masks[entity] |= COMPONENT_TRANSFORM;
    world->transforms[entity] = malloc(sizeof(TransformComponent));
    
    // Initialize with default values
    glm_vec3_zero(world->transforms[entity]->position);
    glm_vec3_zero(world->transforms[entity]->rotation);
    glm_vec3_one(world->transforms[entity]->scale); // Scale of 1 means original size
}

void World_AddPhysicsBodyComponent(World* world, Entity entity) {
    if (entity >= world->count) return;

    world->masks[entity] |= COMPONENT_PHYSICS;
    world->physics_bodies[entity] = malloc(sizeof(PhysicsBodyComponent));
    
    // Use memset to zero out the whole struct initially
    memset(world->physics_bodies[entity], 0, sizeof(PhysicsBodyComponent));

    // Set a default mass of 1.0; zero mass can cause division-by-zero errors.
    world->physics_bodies[entity]->mass = 1.0f;
}

With these changes, our ECS scaffolding is now capable of managing actual game object data!


Conclusion

In this lesson, we have put flesh on the bones of our ECS architecture. We took the abstract idea of "components" and created concrete C struct definitions for two of the most critical ones in any game engine.

Key Takeaways:

  • A TransformComponent should store position, rotation, and scale as separate vectors for maximum flexibility. The final transformation matrix can be computed from these as needed.
  • A PhysicsBodyComponent stores dynamic properties like mass, velocity, acceleration, and accumulated force, providing the data needed for physics simulations.
  • These components are simple C structs defined in a shared components.h file.
  • Our World manager allocates memory for these components and associates them with an entity, tracking their existence with a bitmask.

Preview of the Next Lesson:
We have defined what our objects are, but how do we manage the overall flow of the game? Our next learning outcome is to implement a simple state machine to manage game states (e.g., main menu, playing, paused). This will allow us to control when our entities are being updated and rendered, separating, for example, the logic of the main menu from the logic of the main game loop.