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:
Velocity
, Acceleration
, and Mass
.Tick()
method, including gravity.ApplyForce()
and ApplyImpulse()
- for external factors to influence the entity's motion.PhysicsComponent
with the EntityComponent
and TransformComponent
.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.
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:
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;
}
}
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 or, equivalently, .
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 . 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, ). It directly changes the Velocity
using . 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;
}
}
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:
Velocity
based on the current Acceleration
and the time elapsed (DeltaTime
).Velocity
and DeltaTime
.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};
}
// ...
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 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:
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
IntegrationJust 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;
}
// ...
};
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.
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:
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.
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.
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});
}
// ...
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!
}
// ...
};
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.
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";
}
}
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:
PhysicsComponent
centralizes physics state - Velocity
, Acceleration
, Mass
- and behavior -Tick()
, ApplyForce()
, ApplyImpulse()
.TransformComponent
to read and write the entity's position.Tick()
method applies the equations of motion: velocity updates from acceleration, and position updates from velocity.Tick()
or a helper function.ApplyForce()
modifies acceleration (), affecting velocity over time.ApplyImpulse()
modifies velocity directly (), causing instant changes.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.
Integrate basic physics simulation into entities using a dedicated component
Learn C++ and SDL development by creating hands on, practical projects inspired by classic retro games