This is part of an ongoing series where we write a complete 2D game engine in C++ and SFML. A new tutorial is released every Monday. You can find the complete list of tutorials here and download the source code from the projects GitHub page.

In this weeks tutorial, we start work on a camera component for our game. We’ll start simply by implementing the functionality to follow the player around the level on the x-axis (the y-axis will be locked in place for now). As we develop the game we’ll want a more advanced camera features such as the ability to follow the player at a variable speed, camera shake, and locking to the level edge so that we don’t see area beyond our level. But starting small will help us get the hang of how SFML’s Views work (more on that shortly) and provide a refresher of how we create and add components to our objects.

To help us with our camera component we’ll make use of SFML’s Views. I’ll go into more detail about how they work when we start doing more advanced things with them but for now, you can think of them as a 2D camera (and they are described as such in the documentation). Using sf::View we can rotate and zoom the scene and also easily move the ‘camera’ around, which is the exact functionality we need for today.

Before we can take advantage of the features offered by sf::Views we need a method of getting/setting our windows view. To accomplish this I’ve added a couple of functions to our Window class.

Window.hpp
class Window
{
public:


const sf::View& GetView() const;
void SetView(const sf::View& view);
};

Window.cpp
const sf::View& Window::GetView() const
{
return window.getView();
}

void Window::SetView(const sf::View& view)
{
window.setView(view);
}

I have also updated GetViewSpace to take advantage of the new GetView method.

Window.cpp
sf::FloatRect Window::GetViewSpace() const
{
const sf::View& view = GetView();
const sf::Vector2f& viewCenter = view.getCenter();
const sf::Vector2f& viewSize = view.getSize();
sf::Vector2f viewSizeHalf(viewSize.x * 0.5f, viewSize.y * 0.5f);
sf::FloatRect viewSpace(viewCenter - viewSizeHalf, viewSize);
return viewSpace;
}

With that our of the way, we can focus on our camera component. Create a new class called C_Camera. Like all components I’ve prepended the class name with C_.

C_Camera.hpp
#ifndef C_Camera_hpp
#define C_Camera_hpp

#include "Component.hpp"

class C_Camera : public Component
{
public:
C_Camera(Object* owner);

void LateUpdate(float deltaTime) override;

void SetWindow(Window* gameWindow);

private:
Window* window;
};

#endif /* C_Camera_hpp */

We override the LateUpdate function of the Component class and create a SetWindow function as we’ll need access to the windows View.

C_Camera.cpp
#include "C_Camera.hpp"
#include "Object.hpp"

C_Camera::C_Camera(Object* owner) : Component(owner) { }

void C_Camera::LateUpdate(float deltaTime)
{
if(window)
{
sf::View view = window->GetView();

const sf::Vector2f& targetPos = owner->transform->GetPosition();
//TODO: remove hard-coding of y value
view.setCenter(targetPos.x, 500);

window->SetView(view);
}
}

void C_Camera::SetWindow(Window* gameWindow)
{
window = gameWindow;
}

The LateUpdate function retrieves the object’s position (whichever object we add the component to) and sets the Views centre x position to that of the objects x position. We’ve hardcoded the y value at 500 for now as this lines up with the bottom of our level, we’ll change this in future. Once we’ve changed the View we set the windows View. Whenever we make changes to the windows View we’ll follow this three-step process:

  1. Retrieve the View.
  2. Edit the View.
  3. Update the windows View with the modified View.

We use LateUpdate rather than Update because we assume that the object’s movement will be processed in Update and we want the final object’s position for that frame. We need any class that instantiates the camera component to have access to the window, which means our SceneGame class will also need it so it can pass it to the camera.

SceneGame.hpp
#include “C_Camera.hpp"

class SceneGame : public Scene
{
public:
// Pass a reference to window in the constructor.
SceneGame(WorkingDirectory& workingDir,
ResourceAllocator<sf::Texture>& textureAllocator, Window& window);

private:

// Store a reference to the window class.
Window& window;
};

SceneGame.cpp
SceneGame::SceneGame(WorkingDirectory& workingDir, 
ResourceAllocator<sf::Texture>& textureAllocator,
Window& window)
: workingDir(workingDir), textureAllocator(textureAllocator), 
  mapParser(textureAllocator), window(window) { }

We’ll now need to pass the window as a parameter when we create the game scene in the Game class.

SceneGame.cpp
Game::Game() : window("that game engine")
{

std::shared_ptr<SceneGame> gameScene =
std::make_shared<SceneGame>(workingDir, textureAllocator, window);

}

Now our game scene has access to the window we can create the camera component and add it to the player object.

SceneGame.cpp
void SceneGame::OnCreate()
{


auto camera = player->AddComponent<C_Camera>();
camera->SetWindow(&window);

objects.Add(player);


}

With that done we can now move around freely and the camera will follow us for the ride, so we can finally see the end of our level!
The end of the level at last
The end of the level at last!

You may have noticed that we can still leave the top and bottom of the screen because we have hardcoded the y-axis value. But don’t worry we’ll change that shortly. You may have also noticed that collisions are not working in the second half of the level. This is because we have not set the size of the quadtree correctly to encompass the whole level. To help us select the right size for the quadtree it would be useful if we could zoom the camera in and out so that we could see the whole level at once, which is why camera zoom will be the focus of next weeks tutorial.

As always, if you have any suggestions for what you would like covered or are having any trouble implementing a feature, then let me know in the comments and I’ll get back to you as soon as I can. 

Thank you for reading 🙂