Creating a Physics Component

Integrate basic physics simulation into entities using a dedicated component

Ryan McCombe
Updated

In this lesson, we'll enhance our entity-component system by adding basic physics simulation. We'll create a dedicated PhysicsComponent responsible for managing an entity's physical properties and behavior. You'll learn how to:

  • Store and manage physical state like Velocity, Acceleration, and Mass.
  • Implement core physics updates within the component's Tick() method, including gravity.
  • Provide methods - ApplyForce() and ApplyImpulse() - for external factors to influence the entity's motion.
  • Integrate the PhysicsComponent with the EntityComponent and TransformComponent.
  • Connect player input to the physics system using commands.

By the end, you'll have a reusable component that allows entities to move realistically under the influence of forces like gravity and player input.

Starting Point

Before we build our new PhysicsComponent, let's review the relevant parts of our existing entity-component structure. Our foundation includes an Entity class that manages a collection of Component objects.

We already have components like TransformComponent (handling position and scale), ImageComponent (rendering visuals), and InputComponent (processing player input via commands). The Component base class provides common functionality and interfaces.

Below are the key files we'll be building upon. Familiarity with these components, especially Entity, Component, and TransformComponent, will be helpful as we integrate physics. The current version of those classes are provided below:

Note that this lesson is using concepts we covered in our physics section earlier in the course, so familiarity with those topics is recommended:

Physical Motion

Create realistic object movement by applying fundamental physics concepts

Force, Drag, and Friction

Learn how to implement realistic forces like gravity, friction, and drag in our physics simulations

Momentum and Impulse Forces

Add explosions and jumping to your game by mastering momentum-based impulse forces

Creating a PhysicsComponent

We'll start by defining the header file for our new component, PhysicsComponent.h. It will inherit from our base Component class and contain members to store the entity's physical state: Velocity, Acceleration, and Mass.

// PhysicsComponent.h
#pragma once
#include "Component.h"
#include "Vec2.h"

class PhysicsComponent : public Component {
 public:
  // Inherit constructor
  using Component::Component;

  Vec2 GetVelocity() const { return Velocity; }
  void SetVelocity(const Vec2& NewVelocity) {
    Velocity = NewVelocity;
  }
  
  float GetMass() const { return Mass; }
  void SetMass(float NewMass);

 private:
  Vec2 Velocity{0.0, 0.0}; // m/s
  Vec2 Acceleration{0.0, 0.0}; // m/s^2
  float Mass{1.0}; // kg, default to 1kg
};
// PhysicsComponent.cpp
#include <iostream>
#include "PhysicsComponent.h"

void PhysicsComponent::SetMass(float NewMass) {
  if (NewMass <= 0.0) {
    std::cerr << "Error: Mass must be positive. "
                 "Setting to 1.0kg instead.\n";
    Mass = 1.0;
  } else {
    Mass = NewMass;
  }
}

Applying Forces and Impulses

We need to implement the methods that allow external factors (like player input commands, explosions, or collisions) to affect the physics state.

As a reminder, a force affects the Acceleration, which in turn influences Velocity. The relationship between force, mass, and acceleration is F=MA\text{F=MA} or, equivalently, A = F/M\text{A = F/M}.

In contrast, an impulse directly changes Velocity directly. To calculate the velocity change caused by an impulse, we divide the impulse by the mass.

Let's implement our force handling as a public ApplyForce() function. It takes a force vector (in Newtons) and converts it into acceleration using A = F/M\text{A = F/M}. This acceleration is added to the component's current Acceleration, which will then influence the Velocity change during the next Tick(). We'll add Tick() in the next section, but let's add ApplyForce() now:

// PhysicsComponent.h
// ...

class PhysicsComponent : public Component {
 public:
  // ...

  // Apply force (in Newtons) - affects acceleration
  void ApplyForce(const Vec2& Force);
  
  // ...
};
// PhysicsComponent.cpp
// ...

void PhysicsComponent::ApplyForce(
  const Vec2& Force
) {
  // A = F/M
  if (Mass > 0.0f) { // Avoid division by zero
    Acceleration += Force / Mass;
  }
}

ApplyImpulse() takes an impulse vector (change in momentum, kgm/skg \cdot m/s). It directly changes the Velocity using Δv=Impulse / M\Delta v = \text{Impulse / M}. This causes an instantaneous change in speed/direction.

// PhysicsComponent.h
// ...

class PhysicsComponent : public Component {
 public:
  // ...
   
  // Apply impulse - affects velocity directly
  void ApplyImpulse(const Vec2& Impulse);
  
  // ...
};
// PhysicsComponent.cpp
// ...

void PhysicsComponent::ApplyImpulse(
  const Vec2& Impulse
) {
  // Change in Velocity = Impulse / Mass
  if (Mass > 0.0f) { // Avoid division by zero
    Velocity += Impulse / Mass;
  }
}

Physics Update Logic - Tick()

The core of our physics simulation happens in the Tick() method. Let's override it:

// PhysicsComponent.h
// ...

class PhysicsComponent : public Component {
 public:
  // ...
  void Tick(float DeltaTime) override;
  // ...
};

For the implementation, here is the sequence of actions that our Tick() function needs to perform:

  1. Apply Persistent Forces Gravity: We need to apply acceleration from forces that continuously act on our entity, such as gravity.
  2. Update Velocity: Adjust the current Velocity based on the current Acceleration and the time elapsed (DeltaTime).
  3. Update Position: Adjust the entity's position based on the new Velocity and DeltaTime.
  4. Reset Acceleration: Clear the acceleration for the next frame. Forces applied during the next tick will rebuild the acceleration.

This method needs to interact with the TransformComponent to get and set the entity's position using the helper functions we previously added to the base Component class - GetOwnerPosition() and SetOwnerPosition():

// PhysicsComponent.cpp
// ...

void PhysicsComponent::Tick(float DeltaTime) {
  // Define gravity constant
  const Vec2 GRAVITY{0.0f, -9.8f};

  // 1. Apply persistent forces like gravity
  //    See note below
  ApplyForce(GRAVITY * Mass);

  // 2. Update velocity based on acceleration
  Velocity += Acceleration * DeltaTime;

  // 3. Update position based on velocity
  //    Get current position, add velocity
  //    and set new position
  SetOwnerPosition(
    GetOwnerPosition() + Velocity * DeltaTime
  );

  // 4. Reset acceleration for the next frame.
  //    Forces applied before the next Tick
  //    will accumulate here.
  Acceleration = {0.0, 0.0};
}

// ...

Dependencies and Initialization

Physics calculations inherently rely on the entity having a position in the world. Therefore, our PhysicsComponent depends on a TransformComponent being present on the same Entity.

Let's override the Initialize() method to enforce this dependency. As with our other components that rely on a transform, if no TransformComponent is found, we'll log an error and requests our own own removal:

// PhysicsComponent.h
// ...

class PhysicsComponent : public Component {
 public:
  // ...
  void Initialize() override;
  // ...
};
// PhysicsComponent.cpp
#include <iostream>
#include "PhysicsComponent.h"
#include "Entity.h" // For GetOwner()

void PhysicsComponent::Initialize() {
  // Physics needs a Transform to know where
  // the entity is
  if (!GetOwner()->GetTransformComponent()) {
    std::cerr << "Error: PhysicsComponent "
      "requires TransformComponent on its Owner.\n";

    // Request self-removal
    GetOwner()->RemoveComponent(this);
  }
}

// ...

Integrating with Entity

Just like our other components, we need to integrate PhysicsComponent into the Entity class. Let's add AddPhysicsComponent() and GetPhysicsComponent() methods to Entity.h, following the familiar pattern.

As with the TransformComponent, an entity should have, at most, one PhysicsComponent. If our entity already has a PhysicsComponent, AddPhysicsComponent() will log an error and return without adding another:

// Entity.h
// ...
#include "PhysicsComponent.h"
// ...

class Entity {
public:
  // ...

  PhysicsComponent* AddPhysicsComponent() {
    if (GetPhysicsComponent()) {
      std::cerr << "Error: Cannot add multiple "
        "PhysicsComponents to an Entity.\n";
      return nullptr;
    }

    ComponentPtr& NewComponent{
      Components.emplace_back(
        std::make_unique<PhysicsComponent>(this))
    };

    NewComponent->Initialize();
    return static_cast<PhysicsComponent*>(
      NewComponent.get());
  }

  PhysicsComponent* GetPhysicsComponent() const {
    for (const ComponentPtr& C : Components) {
      if (auto Ptr{
        dynamic_cast<PhysicsComponent*>(C.get())
      }) {
        return Ptr;
      }
    }
    return nullptr;
  }

  // ...
};

Example Usage

Let's see it in action. We'll update Scene.h to create an entity with TransformComponent and PhysicsComponent. We'll give it an initial velocity and mass:

// Scene.h
// ...

class Scene {
 public:
  Scene() {
    EntityPtr& Player{Entities.emplace_back(
      std::make_unique<Entity>(*this))};

    Player->AddTransformComponent()
          ->SetPosition({2, 2});

    // Add physics
    PhysicsComponent* Physics{
      Player->AddPhysicsComponent()};
      
    // Make it 50kg
    Physics->SetMass(50);
    // Set initial velocity
    Physics->SetVelocity({5, 7});

    // Add an image to see it
    Player->AddImageComponent("player.png");
  }
  // ...
};

If you run this, you should see the player entity start at {2, 2}, moving up and to the right due to its initial velocity, and fall downwards due to the gravity applied by the PhysicsComponent.

Note that this screenshot includes an additional line showing how our entity's trajectory. We cover how to create this line in the following note for those interested.

Input Integration

As a final example, let's update our InputComponent to interact with our physics component. For reference, our InputComponent files are provided below:

To have our inputs apply physics, we'll modify the Command objects created by our InputComponent to interact with the PhysicsComponent.

Instead of a command directly calling TransformComponent::Move(), it should now interact with our new PhysicsComponent instead, applying forces, impulses, or setting the velocity directly. There are many ways we can do this, and the best approach depends on the game feel we're aiming for.

Moving using Forces

In this example, we'll set the entity's velocity directly but, whatever approach we use, we'll implement it through the MovementCommand we created in our input component section.

Our MovementCommand declaration doesn't require any changes. However, let's update our variable's name to Velocity to make it slightly clearer that it's going to be setting the physic's component's Velocity rather than the transform component's Position:

// Commands.h
// ...

class MovementCommand : public Command {
public:
  // Constructor now takes a Force vector
  MovementCommand(Vec2 Velocity) 
  : Velocity(Velocity) {} 

  void Execute(Entity* Target) override;
  Vec2 Velocity; 
};

Over in the definition, we'll update its Execute() method in Commands.cpp to work with the PhysicsComponent.

As always, there are many ways we can do this depending on the movement mechanics we're going for. In this example, we'll completely replace the entity's horizontal velocity based on our input, but we'll keep the existing vertical velocity:

// Commands.cpp
#include "Commands.h"
#include "Entity.h"
#include "TransformComponent.h"
#include "PhysicsComponent.h"

void MovementCommand::Execute(Entity* Target) {
  if (!Target) return;  // Safety Check
  PhysicsComponent* Physics{
    Target->GetPhysicsComponent()};
  if (Physics) {
    Physics->SetVelocity({
      Velocity.x,
      Physics->GetVelocity().y
    });
  } else {
    std::cerr << "Error: MovementCommand "
      "requires a PhysicsComponent on entity\n";
  }
}

As a reminder, our MovementCommand objects are currently created using the command factories in InputComponent.cpp.

These don't need to change in this case, as our MovementCommand constructor hasn't changed. However, we can introduce a SPEED variable to reinforce what this constructor argument represents:

// InputComponent.cpp
// ...

namespace{
// Define movement speed (example value)
const float SPEED{5.0};  

// Factory function for moving left
CommandPtr CreateMoveLeftCommand() {
  return std::make_unique<MovementCommand>( 
    Vec2{-SPEED, 0.0}); 
}

// Factory function for moving right
CommandPtr CreateMoveRightCommand() {
  return std::make_unique<MovementCommand>( 
    Vec2{SPEED, 0.0}); 
}

// ...

Jumping using Impulses

Let's add a new JumpCommand by using our new impulse feature provided by the PhysicsComponent. It's declaration is almost identical to MovementCommand - we'll just use a different variable name for the Vec2:

// Commands.h
// ...

class JumpCommand : public Command {
 public:
  JumpCommand(Vec2 Impulse)
      : Impulse(Impulse) {}
  void Execute(Entity* Target) override;
  Vec2 Impulse;
};

Its definition is also similar - the only difference for now is that we call ApplyImpulse() instead of SetVelocity()

// Commands.cpp
// ...

void JumpCommand::Execute(Entity* Target) {
  if (!Target) return;  // Safety Check
  PhysicsComponent* Physics{
    Target->GetPhysicsComponent()};
  if (Physics) {
    Physics->ApplyImpulse(Impulse);
  } else {
    std::cerr << "Error: JumpCommand "
      "requires a PhysicsComponent on entity\n";
  }
}

Over in InputComponent.cpp, let's add our CreateJumpCommand() factory:

// InputComponent.cpp
// ...

namespace{
// ...

CommandPtr CreateJumpCommand() {
  // Example value in kg*m/s
  const float JUMP_IMPULSE_MAGNITUDE{350.0};   
  // Return a jump command instead of movement
  return std::make_unique<JumpCommand>(
    Vec2{0.0, JUMP_IMPULSE_MAGNITUDE});
}
}

// ...

And update InputComponent::Initialize() to bind the spacebar to the jump command factory by default:

// InputComponent.cpp
// ...

void InputComponent::Initialize() {
  BindKeyHeld(SDLK_LEFT, CreateMoveLeftCommand);
  BindKeyHeld(SDLK_RIGHT, CreateMoveRightCommand);
  // Bind Space to Jump
  BindKeyDown(SDLK_SPACE, CreateJumpCommand); 
}

// ...

Now, pressing left/right applies horizontal movement, and pressing space applies an instantaneous upward impulse, all managed through the PhysicsComponent.

The player entity needs an InputComponent added in the Scene constructor for this to work:

// Scene.h
// ...

class Scene{
public:
  Scene() {
    EntityPtr& Player{Entities.emplace_back(
      std::make_unique<Entity>(*this))};

    Player->AddTransformComponent()
          ->SetPosition({4, 5});
    Player->AddPhysicsComponent()
          ->SetMass(70.0)
    Player->AddImageComponent("player.png");
    Player->AddInputComponent(); // Add input! 
  }
  // ...
};

Complete Code

Our complete PhysicsComponent is provided below:

We also updated our Entity class with new AddPhysicsComponent() and GetPhysicsComponent() functions:

Finally, we updated our InputComponent and its related commands. We added and bound a JumpCommand, and updated our existing MovementCommand to use the entity's PhysicsComponent:

Summary

In this lesson, we created a dedicated PhysicsComponent to implement physical simulation for entities that require it.

This component manages an entity's velocity, acceleration, and mass, applying physics updates each tick and providing methods to react to external forces and impulses.

Key takeaways:

  • A PhysicsComponent centralizes physics state - Velocity, Acceleration, Mass - and behavior -Tick(), ApplyForce(), ApplyImpulse().
  • It depends on TransformComponent to read and write the entity's position.
  • The Tick() method applies the equations of motion: velocity updates from acceleration, and position updates from velocity.
  • Resetting acceleration each frame is crucial for correctly accumulating forces applied during the next frame.
  • Gravity can be applied consistently as a force scaled by mass within the Tick() or a helper function.
  • ApplyForce() modifies acceleration (A = F/M\text{A = F/M}), affecting velocity over time.
  • ApplyImpulse() modifies velocity directly (Δv=Impulse / M\Delta v = \text{Impulse / M}), causing instant changes.
  • Input commands can now target the PhysicsComponent (setting velocity or applying forces and impulses) rather than directly manipulating the TransformComponent.

With this component, our entities can now move and react to forces in a physically plausible way, managed cleanly within our framework. The next step is to handle interactions between these physical entities using collision detection.

Next Lesson
Lesson 120 of 129

Creating a Collision Component

Enable entities to detect collisions using bounding boxes managed by a dedicated component.

Have a question about this lesson?
Purchase the course to ask your own questions