Creating a Physics Component

Integrate basic physics simulation into entities using a dedicated component
This lesson is part of the course:

Game Dev with SDL2

Learn C++ and SDL development by creating hands on, practical projects inspired by classic retro games

Get Started for Free
Abstract art representing computer programming
Ryan McCombe
Ryan McCombe
Posted

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:

#pragma once
#include <memory>
#include <vector>
#include <SDL.h>
#include "Component.h"
#include "TransformComponent.h"
#include "InputComponent.h"
#include "Commands.h"
#include "ImageComponent.h"

class Scene;
using ComponentPtr = std::unique_ptr<Component>;
using ComponentPtrs = std::vector<ComponentPtr>;

class Entity {
public:
  Entity(Scene& Scene) : OwningScene{Scene} {}
  Scene& GetScene() const { return OwningScene; }

  virtual void HandleEvent(const SDL_Event& E) {
    for (ComponentPtr& C : Components) {
      C->HandleEvent(E);
    }
  }

  virtual void Tick(float DeltaTime) {
    for (ComponentPtr& C : Components) {
      C->Tick(DeltaTime);
    }
  }

  virtual void Render(SDL_Surface* Surface) {
    for (ComponentPtr& C : Components) {
      C->Render(Surface);
    }
    for (ComponentPtr& C : Components) {
      C->DrawDebugHelpers(Surface);
    }
  }

  virtual ~Entity() = default;
  
  virtual void HandleCommand(
    std::unique_ptr<Command> Cmd
  ) {
    Cmd->Execute(this);
  }

  TransformComponent* AddTransformComponent() {
    if (GetTransformComponent()) {
      std::cout << "Error: Cannot have "
        "multiple transform components";
      return nullptr;
    }

    std::unique_ptr<Component>& NewComponent{
      Components.emplace_back(
        std::make_unique<
          TransformComponent>(this))};

    NewComponent->Initialize();

    return static_cast<TransformComponent*>(
      NewComponent.get());
  }

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

  ImageComponent* AddImageComponent(
    const std::string& FilePath
  ) {
    std::unique_ptr<Component>& NewComponent{
      Components.emplace_back(
        std::make_unique<ImageComponent>(
          this, FilePath))};

    NewComponent->Initialize();

    return static_cast<ImageComponent*>(
      NewComponent.get());
  }

  using ImageComponents =
    std::vector<ImageComponent*>;
  ImageComponents GetImageComponents() const {
    ImageComponents Result;
    for (const ComponentPtr& C : Components) {
      if (auto Ptr{dynamic_cast<
        ImageComponent*>(C.get())}
      ) {
        Result.push_back(Ptr);
      }
    }
    return Result;
  }

  InputComponent* AddInputComponent() {
    if (GetInputComponent()) {
      std::cout << "Error: Cannot have "
        "multiple input components";
      return nullptr;
    }

    std::unique_ptr<Component>& NewComponent{
      Components.emplace_back(
        std::make_unique<
          InputComponent>(this))};

    NewComponent->Initialize();

    return static_cast<InputComponent*>(
      NewComponent.get());
  }

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

  void RemoveComponent(Component* PtrToRemove) {
    for (size_t i{0}; i < Components.size(); ++i) {
      if (Components[i].get() == PtrToRemove) {
        Components.erase(Components.begin() + i);
        return;
      }
    }

    std::cout << "Warning: Attempted to remove "
      "a component not found on this entity.\n";
  }

protected:
  ComponentPtrs Components;
  Scene& OwningScene;
};
// Component.h
// ...
#pragma once
#include <SDL.h>

class Entity;
class Scene;
class AssetManager;
class Vec2;

class Component {
 public:
  Component(Entity* Owner) : Owner(Owner) {}
  virtual void Initialize() {}
  virtual void HandleEvent(const SDL_Event& E) {}
  virtual void Tick(float DeltaTime) {}
  virtual void Render(SDL_Surface* Surface) {}
  virtual void DrawDebugHelpers(
    SDL_Surface* Surface) {}
  virtual ~Component() = default;

  Entity* GetOwner() const { return Owner; }
  Scene& GetScene() const;
  AssetManager& GetAssetManager() const;
  Vec2 ToScreenSpace(const Vec2& Pos) const;
  Vec2 GetOwnerPosition() const;
  void SetOwnerPosition(const Vec2& Pos) const;
  Vec2 GetOwnerScreenSpacePosition() const;

  float GetOwnerScale() const;

private:
  Entity* Owner{nullptr};
};
#include "Component.h"
#include "Entity.h"
#include "Scene.h"

Scene& Component::GetScene() const {
  return GetOwner()->GetScene();
}

AssetManager& Component::GetAssetManager() const {
  return GetScene().GetAssetManager();
}

Vec2 Component::ToScreenSpace(const Vec2& Pos) const {
  return GetScene().ToScreenSpace(Pos);
}

Vec2 Component::GetOwnerPosition() const {
  TransformComponent* Transform{
    GetOwner()->GetTransformComponent()};
  if (!Transform) {
    std::cerr << "Error: attempted to get position"
      " of an entity with no transform component\n";
    return {0, 0};
  }
  return Transform->GetPosition();
}

void Component::SetOwnerPosition(const Vec2& Pos) const {
  TransformComponent* Transform{
    GetOwner()->GetTransformComponent()};
  if (!Transform) {
    std::cerr << "Error: attempted to set position"
      " of an entity with no transform component\n";
  } else {
    Transform->SetPosition(Pos);
  }
}

Vec2 Component::GetOwnerScreenSpacePosition() const {
  return ToScreenSpace(GetOwnerPosition());
}

float Component::GetOwnerScale() const {
  TransformComponent* Transform{
    GetOwner()->GetTransformComponent()};
  if (!Transform) {
    std::cerr << "Error: attempted to get scale"
      " of an entity with no transform component\n";
    return 1.0;
  }
  return Transform->GetScale();
}
#pragma once
#include <SDL.h>
#include "Utilities.h"
#include "Vec2.h"
#include "Component.h"

class TransformComponent : public Component {
 public:
  using Component::Component;

  Vec2 GetPosition() const { return Position; }
  void SetPosition(const Vec2& NewPosition) {
    Position = NewPosition;
  }

  float GetScale() const { return Scale; }    
  void SetScale(float NewScale) {
    Scale = NewScale;
  }

  void DrawDebugHelpers(SDL_Surface* S) override {
    auto [x, y]{ToScreenSpace(Position)};
    SDL_Rect Square{Utilities::Round({
      x - 10, y - 10, 20, 20
    })};
    SDL_FillRect(S, &Square, SDL_MapRGB(
      S->format, 255, 0, 0));
  }

 private:
  Vec2 Position{0, 0};
  float Scale{1.0f};
};

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

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};
}

// ...

Gravitational Force

In our previous chapter, we applied gravity directly to the Acceleration variable but, in this component, we’re instead applying it in the form of a force.

This is functionally equivalent, however, we should note that the gravitational force scales in proportion with the object’s mass, which is why we perform this multiplication:

ApplyForce(GRAVITY * Mass);

Within our ApplyForce() method (which we implement next) we calculate acceleration by dividing forces by the entity’s mass, as per the A=F/M\text{A=F/M} relationship.

This multiplication and division cancels out, so the effect of ApplyForce(GRAVITY * Mass) is equivalent to just adding GRAVITY directly to the Acceleration:

Acceleration += GRAVITY;

We covered the gravitational force in a dedicated section in our earlier lesson:

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);
  }
}

// ...

Entity Integration

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.

Screenshot showing the trajectory of an entity

Drawing Trajectory Indicators

Drawing debug helpers that persist across multiple frames is a little more complicated, because our window surface is cleared of all its historic content in every iteration of our application loop. This is done in the GameWindow.Render() call, which overwrites all of our pixels with a solid gray color.

To draw content that persists across multiple frames, we need to draw it on a different surface. For example, let’s create an SDL_Surface in our Scene and:

// Scene.h
// ...

class Scene {
 public:
  // ...
  
  #ifdef DRAW_DEBUG_HELPERS
  SDL_Surface* Trajectories{
    SDL_CreateRGBSurfaceWithFormat(
      0, 700, 300, 32, SDL_PIXELFORMAT_RGBA32
    )
  };
  #endif
  
  // ...
};

Note that this surface assumes our window dimensions are 700x300. We’re also neglecting to safely manage the memory of this surface by calling SDL_FreeSurface(). We assume we only ever have one Scene and that it survives until the end of our application, at which point the surface will be freed by SDL_Quit().

In a larger project, we’d want to be more robust here, but let’s continue for the sake of this example.

To see the latest content of our Trajectories surface, let’s blit it into our window surface on every frame. The window surface is currently being provided to the Render() function on every frame:

// Scene.h
// ...

class Scene {
 public:
  // ...
  
  void Render(SDL_Surface* Surface) {
    SDL_GetClipRect(Surface, &Viewport);
    for (EntityPtr& Entity : Entities) {
      Entity->Render(Surface);
    }

    #ifdef DRAW_DEBUG_HELPERS
    SDL_BlitSurface(
      Trajectories, nullptr, Surface, nullptr
    );
    #endif
  }
  
  // ...
};

The content of this Trajectories surface is never cleared - if we draw something on the surface, it remains there until something else overwrites it.

Any component with access to the Scene can access this public Trajectories surface. Let’s update our PhysicsComponent to draw onto this surface. We’ll override DrawDebugHelpers():

// PhysicsComponent.h
// ...

class PhysicsComponent : public Component {
 public:
  // ...
  void DrawDebugHelpers(
    SDL_Surface* Surface) override;
  // ...
};

Within the implementation, we’ll draw a small blue rectangle at our owner’s current screen space position:

// PhysicsComponent.cpp
// ...
#include "Utilities.h"

// ...

void PhysicsComponent::DrawDebugHelpers(
  SDL_Surface* Surface
) {
#ifdef DRAW_DEBUG_HELPERS
  auto [x, y]{GetOwnerScreenSpacePosition()};
  SDL_Rect PositionIndicator{
    int(x) - 2, int(y) - 2, 4, 4};
  SDL_FillRect(
    GetScene().Trajectories,
    &PositionIndicator,
    SDL_MapRGB(
      GetScene().Trajectories->format,
      0, 0, 255
    )
  );
#endif
}

Our collection of frame-by-frame rectangles will now accumulate on our Trajectories surface over time. And, on every frame, the full contents of the Trajectories surface is blitted onto our window surface, allowing us to view the movement history of our entities:

Screenshot showing the trajectory of an entity

Static Entities

In our earlier physics lessons, our entities included a boolean value indicating whether they were movable by physical forces such as gravity:

// GameObject.cpp
// ...

void GameObject::Tick(float DeltaTime) {
  if (!isMovable) {
    // Skip physics simulation
    return; 
  };
  
  // Do physics stuff...
}

// ...

This is much easier to replicate in our component-based system. If we don’t want an entity to be affected by physics, we simply don’t give it a physics component.

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:

#pragma once
#include <functional>
#include <memory>
#include <unordered_map>
#include <SDL.h>
#include "Component.h"

class Command;
using CommandPtr = std::unique_ptr<Command>;
using CommandFactory = std::function<
  CommandPtr()>;
using KeyToFactoryMap = std::unordered_map<
  SDL_Keycode, CommandFactory>;

class InputComponent : public Component {
 public:
  using Component::Component;
  void Initialize() override;
  void Tick(float DeltaTime) override;

  void HandleEvent(const SDL_Event& E) override;

  void BindKeyDown(
    SDL_Keycode Key, CommandFactory Factory
  ) {
    KeyDownBindings[Key] = Factory;
  }

  void BindKeyHeld(
    SDL_Keycode Key, CommandFactory Factory
  ) {
    KeyHeldBindings[Key] = Factory;
  }

private:
  // Map for discrete key presses (events)
  KeyToFactoryMap KeyDownBindings;
  // Map for continuous key holds (polling)
  KeyToFactoryMap KeyHeldBindings;
};
#include <SDL.h>
#include "InputComponent.h"
#include "Entity.h"
#include "Commands.h"
#include "Vec2.h"

namespace{
CommandPtr CreateMoveLeftCommand() {
  return std::make_unique<MovementCommand>(
    Vec2{-5.0, 0.0});
}

CommandPtr CreateMoveRightCommand() {
  return std::make_unique<MovementCommand>(
    Vec2{5.0, 0.0});
}
}

void InputComponent::Initialize() {
  BindKeyHeld(SDLK_LEFT, CreateMoveLeftCommand);
  BindKeyHeld(SDLK_RIGHT, CreateMoveRightCommand);
  // Bind other keys...
}

void InputComponent::Tick(float DeltaTime) {
  Entity* Owner{GetOwner()};
  if (!Owner) return; // Safety check

  // Get the current keyboard state
  const Uint8* CurrentKeyStates{
    SDL_GetKeyboardState(nullptr)};

  // Check bindings for keys being held down
  for (const auto& [Key, Factory] : KeyHeldBindings) {
    SDL_Scancode Scancode{SDL_GetScancodeFromKey(Key)};
    if (CurrentKeyStates[Scancode]) {
      // Key is held, create and handle command
      Owner->HandleCommand(Factory());
    }
  }
}

void InputComponent::HandleEvent(const SDL_Event& E) {
  if (E.type == SDL_KEYDOWN) {
    Entity* Owner{GetOwner()};
    if (!Owner) return;
    SDL_Keycode Key{E.key.keysym.sym};
    if (KeyDownBindings.contains(Key)) {
      Owner->HandleCommand(KeyDownBindings[Key]());
      // Alternatively:
      // KeyDownBindings[Key]()->Execute(Owner);
    }
  }
}
#pragma once
#include "Vec2.h"

class Entity;

class Command {
 public:
  virtual void Execute(Entity* Target) {}
  virtual ~Command() = default;
};

class MovementCommand : public Command {
public:
  MovementCommand(Vec2 Movement)
  : Movement{Movement} {}
  void Execute(Entity* Target) override;
  Vec2 Movement;
};
#include "Commands.h"
#include "Entity.h"
#include "TransformComponent.h"

void MovementCommand::Execute(Entity* Target) {
  if (!Target) return;  // Safety Check
  Target->GetTransformComponent()
        ->Move(Movement);
}

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! 
  }
  // ...
};

Drag and Friction

Our previous physics chapter included drag and friction in our physics simulation. Our new PhysicsComponent does not include those forces. This means that our objects don’t slow down when the movement input ceases.

For an arcade-feeling game where our entity stops moving as soon as the input stops, we can reset the horizontal velocity to 0 at the end of our Tick():

// PhysicsComponent.cpp
// ...

void PhysicsComponent::Tick(float DeltaTime) {
// Reset horizontal velocity for every frame Velocity.x = 0; }

However, resetting our horizontal velocity at the start of every frame can create unnatural motion in other contexts, such as when movement input stops whilst the character is flying through the air.

If we want to implement drag and friction to handle these scenarios more naturally, our earlier lesson introduced the concepts we need:

Remember, the idea of composition is that entities shouldn’t have features they don’t require. Not every entity with a PhysicsComponent requires drag and friction. As such, if our game required these capabilities, it’s usually recommended that they be separate components that can be attached only to the entities that need it.

These hypothetical DragComponent and FrictionComponent types can still depend on the entity having a PhysicsComponent, and interact with that physics component much like how our physics component interacts with the TransformComponent.

// FrictionComponent.h (Conceptual)
// ...

class FrictionComponent : public Component {
 public:
  // ...
  void Tick(float DeltaTime) override {
    // Assume I exist
    PhysicsComponent* Physics{
      GetOwner()->GetPhysicsComponent()
    }
    
    Vec2 Friction{/* Calculate me */};
    
    PhysicsComponent->ApplyForce(Friction);
  }
  // ...
};

This separation has the added benefit of keeping our code easy to work with, preventing our PhysicsComponent from getting large and cumbersome.

Complete Code

Our complete PhysicsComponent is provided below:

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

class PhysicsComponent : public Component {
 public:
  using Component::Component;
  void Initialize() override;

  void Tick(float DeltaTime) override;
  void DrawDebugHelpers(SDL_Surface* Surface) override;

  // Apply force (in Newtons) - affects acceleration
  void ApplyForce(const Vec2& Force);
  // Apply impulse - affects velocity directly
  void ApplyImpulse(const Vec2& Impulse);

  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
};
#include <iostream>
#include "Entity.h"
#include "PhysicsComponent.h"
#include "Scene.h"

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);
  }
}

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;
  }
}

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

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

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};
}

void PhysicsComponent::DrawDebugHelpers(
  SDL_Surface* Surface
) {
#ifdef DRAW_DEBUG_HELPERS
  auto [x, y]{GetOwnerScreenSpacePosition()};
  SDL_Rect PositionIndicator{
    int(x) - 2, int(y) - 2, 4, 4};
  SDL_FillRect(
    GetScene().Trajectories,
    &PositionIndicator,
    SDL_MapRGB(
      GetScene().Trajectories->format, 0, 0, 255
    )
  );
#endif
}

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

#pragma once
#include <memory>
#include <vector>
#include <SDL.h>
#include "Component.h"
#include "TransformComponent.h"
#include "InputComponent.h"
#include "Commands.h"
#include "ImageComponent.h"
#include "PhysicsComponent.h"

class Scene;
using ComponentPtr = std::unique_ptr<Component>;
using ComponentPtrs = std::vector<ComponentPtr>;

class Entity {
public:
  Entity(Scene& Scene) : OwningScene{Scene} {}
  Scene& GetScene() const { return OwningScene; }

  virtual void HandleEvent(const SDL_Event& E) {
    for (ComponentPtr& C : Components) {
      C->HandleEvent(E);
    }
  }

  virtual void Tick(float DeltaTime) {
    for (ComponentPtr& C : Components) {
      C->Tick(DeltaTime);
    }
  }

  virtual void Render(SDL_Surface* Surface) {
    for (ComponentPtr& C : Components) {
      C->Render(Surface);
    }
    for (ComponentPtr& C : Components) {
      C->DrawDebugHelpers(Surface);
    }
  }

  virtual ~Entity() = default;

  virtual void HandleCommand(
    std::unique_ptr<Command> Cmd
  ) {
    Cmd->Execute(this);
  }

  TransformComponent* AddTransformComponent() {
    if (GetTransformComponent()) {
      std::cout << "Error: Cannot have "
        "multiple transform components";
      return nullptr;
    }

    std::unique_ptr<Component>& NewComponent{
      Components.emplace_back(
        std::make_unique<
          TransformComponent>(this))};

    NewComponent->Initialize();

    return static_cast<TransformComponent*>(
      NewComponent.get());
  }

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

  ImageComponent* AddImageComponent(
    const std::string& FilePath
  ) {
    std::unique_ptr<Component>& NewComponent{
      Components.emplace_back(
        std::make_unique<ImageComponent>(
          this, FilePath))};

    NewComponent->Initialize();

    return static_cast<ImageComponent*>(
      NewComponent.get());
  }

  using ImageComponents =
    std::vector<ImageComponent*>;
  ImageComponents GetImageComponents() const {
    ImageComponents Result;
    for (const ComponentPtr& C : Components) {
      if (auto Ptr{dynamic_cast<
        ImageComponent*>(C.get())}
      ) {
        Result.push_back(Ptr);
      }
    }
    return Result;
  }

  InputComponent* AddInputComponent() {
    if (GetInputComponent()) {
      std::cout << "Error: Cannot have "
        "multiple input components";
      return nullptr;
    }

    std::unique_ptr<Component>& NewComponent{
      Components.emplace_back(
        std::make_unique<
          InputComponent>(this))};

    NewComponent->Initialize();

    return static_cast<InputComponent*>(
      NewComponent.get());
  }

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

  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;
  }

  void RemoveComponent(Component* PtrToRemove) {
    for (size_t i{0}; i < Components.size(); ++i) {
      if (Components[i].get() == PtrToRemove) {
        Components.erase(Components.begin() + i);
        return;
      }
    }

    std::cout << "Warning: Attempted to remove "
      "a component not found on this entity.\n";
  }

protected:
  ComponentPtrs Components;
  Scene& OwningScene;
};

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:

#include <SDL.h>
#include "InputComponent.h"
#include "Commands.h"
#include "Entity.h"
#include "Vec2.h"

namespace{
const float SPEED{5.0};

CommandPtr CreateMoveLeftCommand() {
  return std::make_unique<MovementCommand>(
    Vec2{-SPEED, 0.0} 
  );
}

CommandPtr CreateMoveRightCommand() {
  return std::make_unique<MovementCommand>(
    Vec2{SPEED, 0.0} 
  );
}

CommandPtr CreateJumpCommand() { 
  const float JUMP_IMPULSE_MAGNITUDE{350.0}; 
  return std::make_unique<JumpCommand>( 
    Vec2{0.0, JUMP_IMPULSE_MAGNITUDE}); 
  } 
} 

void InputComponent::Initialize() {
  BindKeyHeld(SDLK_LEFT, CreateMoveLeftCommand);
  BindKeyHeld(SDLK_RIGHT,
              CreateMoveRightCommand);
  BindKeyDown(SDLK_SPACE, CreateJumpCommand); 
  // Bind other keys...
}

void InputComponent::Tick(float DeltaTime) {
  Entity* Owner{GetOwner()};
  if (!Owner) return; // Safety check

  // Get the current keyboard state
  const Uint8* CurrentKeyStates{
    SDL_GetKeyboardState(nullptr)};

  // Check bindings for keys being held down
  for (const auto& [Key, Factory] :
       KeyHeldBindings) {
    SDL_Scancode Scancode{
      SDL_GetScancodeFromKey(Key)};
    if (CurrentKeyStates[Scancode]) {
      // Key is held, create and handle command
      Owner->HandleCommand(Factory());
    }
  }
}

void InputComponent::HandleEvent(
  const SDL_Event& E) {
  if (E.type == SDL_KEYDOWN) {
    Entity* Owner{GetOwner()};
    if (!Owner) return;
    SDL_Keycode Key{E.key.keysym.sym};
    if (KeyDownBindings.contains(Key)) {
      Owner->HandleCommand(
        KeyDownBindings[Key]());
    }
  }
}
#pragma once
#include "Vec2.h"

class Entity;

class Command {
 public:
  virtual void Execute(Entity* Target) {}
  virtual ~Command() = default;
};

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

class JumpCommand : public Command {
 public:
  JumpCommand(Vec2 Impulse)
    : Impulse(Impulse) {}
  void Execute(Entity* Target) override;
  Vec2 Impulse;
};
#include "Commands.h"
#include "Entity.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";
  }
}

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";
  }
}

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.

Free and Unlimited Access

Professional C++

Unlock the true power of C++ by mastering complex features, optimizing performance, and learning expert workflows used in professional development

Screenshot from Warhammer: Total War
Screenshot from Tomb Raider
Screenshot from Jedi: Fallen Order
Ryan McCombe
Ryan McCombe
Posted
sdl2-promo.jpg
This lesson is part of the course:

Game Dev with SDL2

Learn C++ and SDL development by creating hands on, practical projects inspired by classic retro games

Get Started for Free
Creating Components
sdl2-promo.jpg
This lesson is part of the course:

Game Dev with SDL2

Learn C++ and SDL development by creating hands on, practical projects inspired by classic retro games

This course includes:

  • 108 Lessons
  • 92% Positive Reviews
  • Regularly Updated
  • Help and FAQs
Free and Unlimited Access

Professional C++

Unlock the true power of C++ by mastering complex features, optimizing performance, and learning expert workflows used in professional development

Screenshot from Warhammer: Total War
Screenshot from Tomb Raider
Screenshot from Jedi: Fallen Order
Contact|Privacy Policy|Terms of Use
Copyright © 2025 - All Rights Reserved