Game development in C++

You may have already seen this picture many times, but I decided to insert it here anyway.

You may have already seen this picture many times, but I decided to insert it here anyway.

Introduction

Hi all!

So the idea came to my mind – to make my own game like Vampire Survivors And Brotatoand then I thought that I could also write a series of articles about how I’m developing it, in case someone finds it useful (or at least funny. Or maybe readers will start writing me angry comments under this article and I’ll pay and I'll give up programming, who knows).

Well, actually, here is the first part.
In it I will show how I created a character and taught him to run.

Disclaimer

The author of the article is also learning to program, so there may be (there are) shortcomings and errors in the article, please be understanding and correct them in the comments. I also don’t encourage you to take this article as a guide, these are just my thoughts that I decided to share.

Pre-beginning

I would like to start with how to install SFML, but there are already many Russian-language and other guides on the Internet. Not small, you will find the information on installing it to suit your environment.
I installed it myself in ancient times using vcpkg by analogy with this video.

I will develop using C++17, Microsoft Visual Studio 2019, my wild imagination and a lot of coffee.

Start

I'll start by taking the test code from the SFML website(link), I’ll remove everything unnecessary from it and launch the project – a window will appear.
Here's the code:

// main.cpp

#include <SFML/Graphics.hpp>

int main()
{
    sf::RenderWindow window(sf::VideoMode(200, 200), "SFML works!");

    while (window.isOpen())
    {
        sf::Event event;
        while (window.pollEvent(event))
        {
            if (event.type == sf::Event::Closed)
                window.close();
        }

        window.clear();
        window.display();
    }

    return 0;
}
Here is the result

Here is the result

I’ll tell you in more detail here

sf::RenderWindow – this is the game window, it is used to draw 2D objects. I pass the window dimensions as the first argument, and the title as the second.

From the ninth to the twentieth line is the main cycle of the program, in which we process events, draw graphics, call state update methods, and so on.

At the end of the loop you can see the call window.clear() , this method, oddly enough, clears the window. By default, the window is filled with black, but you can specify the desired color as an argument, for example, white – sf::Color::White .
The method is also called sf::Window::display . It displays everything that has been rendered in the current frame.

A little about Event Loop

Inside the main loop you can see the following structure

sf::Event event;
while (window.pollEvent(event)) {
    if (event.type == sf::Event::Closed) {
        window.close();
    }
}

It processes events from the event queue. What it is? Well, in short, all events, be it resizing a window, clicking the close button, or changing the focus of a window, are placed in a queue from which they are then processed.

You can initialize an event by passing it to a method sf::Window::pollEvent which returns trueif an event was detected in the queue and, accordingly, falseif there were no events.

I will describe the approximate operation of the event loop:

  • Some event comes to the window

  • It writes it to the event queue

  • Each step of the main loop creates a variable in which the first event in the queue is written

  • The method is called sf::Window::pollEvent to which a variable of type is passed by reference sf::Event which needs to be initialized

  • The event is processed based on its type

If you have any questions, you can ask them in the comments or refer to the SFML documentation – here is a link to an article with events

Let's continue to start

It would be nice to create a separate file for project settings, I think there is no need to explain why.
To begin with, I’ll just make a header in which constants with the required values ​​will be stored, and then it will be possible to do something more complex and interesting.

// Constants.h

#pragma once

constexpr float WINDOW_HEIGHT = 720.0;
constexpr float WINDOW_WIDTH  = 1280.0;

I include this file in my main.cpp and instead of setting the window size directly, I use constants

#include <SFML/Graphics.hpp>

#include "include/Engine/Constants.h"

int main() {
    sf::RenderWindow window(sf::VideoMode(WINDOW_WIDTH, WINDOW_HEIGHT), "Title");

    while (window.isOpen()) {
        sf::Event event;
        while (window.pollEvent(event)) {
            if (event.type == sf::Event::Closed) {
                window.close();
            }
        }

        window.clear(sf::Color::White);
        window.display();
    }

    return 0;
}

Creating the Main Character

Hurray, I've finally made a start. Only a few minutes have passed in your article, but I’ve already been sitting for 40 minutes and trying to correctly express my thoughts. Writing an article takes a lot more effort and time than it seems.

Let's start.

There are several types of characters in the game, so it would be nice to create a base class Characterfrom which everything will be inherited, you can add general methods, like getters, to it, and it will be easier to handle interactions with different types of enemies due to polymorphism.
Each character will have a certain amount of hp, size, position, his speed, sprite and the direction in which he moves (in my case there are two – left and right).

// Character.h

#pragma once

#include <SFML/Graphics.hpp>

enum class Direction : bool {
	LEFT = 0,
	RIGHT = 1
};

class Character {
protected:
	float		 m_health;
	float		 m_speed;
	sf::Vector2f m_size;
	sf::Vector2f m_pos;
	sf::Sprite   m_sprite;
	Direction    m_direction = Direction::RIGHT;

public:
	virtual ~Character();

	virtual void Update(float time) = 0;
	void takeDamage(float damage);

	void setPosition(sf::Vector2f& pos);
	void setDirection(Direction direction);

	float getHP() const;
	sf::Vector2f getSize() const;
	sf::Vector2f getPosition() const;
	sf::Sprite getSprite() const;
	Direction getDirection() const;
};

This is what the class looks like: this is the so-called base my character.

It has an abstract method Update() which means that each descendant class must implement it differently, and a set of methods that describe the common functionality of all characters.

Oh yes, you need to explain about the different types from SFML.
Well, for now I’ll tell you a little about those that are used in this article, and I’ll explain about the rest along the way.

  • sf::Vector2f is a class that describes a two-dimensional vector, I use it to store the positions and sizes of objects. Perhaps this application is not entirely correct, but we are on the Internet, I can do whatever I want, you won’t find me.

  • sf::Sprite – a class that describes a sprite, a sprite is a certain graphic object that we can control, change its texture and other properties, about which you can read here

  • sf::Texture – this is a texture class, essentially just a picture that we loaded and can be pulled somewhere

    You can read about the interaction of sprites and textures in SFML here

    Now let's look at the implementation of the class Character:

// Character.cpp

#include "..\include\Engine\Character.h"

Character::~Character() {}

void Character::takeDamage(float damage) {
    m_health -= damage;
}

void Character::setPosition(sf::Vector2f& pos) {
    m_pos = pos;
}

void Character::setDirection(Direction direction) {
    m_direction = direction;
}

float Character::getHP() const {
    return m_health;
}

sf::Vector2f Character::getSize() const {
    return m_size;
}

sf::Vector2f Character::getPosition() const {
    return m_pos;
}

sf::Sprite Character::getSprite() const {
    return m_sprite;
}

Direction Character::getDirection() const {
    return m_direction;
}

There’s not much to comment on here, just setters and getters, so let’s move on to creating a player class.

// Player.h

#pragma once

#include "Engine/Character.h"

class PlayerController;

enum class State {
    IDLE,
    RUN
};

class Player : public Character {
private:
    State             m_state;
    PlayerController* m_controller;

public:
    Player() = delete;
    Player(sf::Texture& texture, sf::Vector2f start_pos, float health);
    ~Player();

    void Update(float time) override;

    void setState(State state);
};

There doesn’t seem to be anything complicated here either, don’t pay attention to the class for now PlayerController I'll tell you about him a little later.
Player It differs from the base class only in that it has its own state, which it stores; this is needed for future rendering of animations.

Let's look at the implementation

// Player.cpp

#include "../include/Player.h"
#include "../include/Engine/PlayerController.h"

Player::Player(sf::Texture& texture, sf::Vector2f start_pos, float health) {
    m_pos = start_pos;
    m_health = health;

    m_controller = PlayerController::getPlayerController();
  
    m_sprite.setTexture(texture)
    m_size = sf::Vector2f(m_sprite.getTextureRect().width, m_sprite.getTextureRect().height);
}

Player::~Player() {}

void Player::Update(float time) {
    m_state = State::IDLE;
    m_controller->controllPlayer(this, time);

    if (m_state == State::RUN) {

    }
    else {

    }

    m_sprite.setPosition(m_pos);
}

void Player::setState(State state) {
    m_state = state;
}

We initialize all fields of the class, field m_size assign a value based on the size of the loaded texture.
In method Update we update the player’s state and change the position of the sprite, there are a lot of interesting things you can do here, but for now we’ll limit ourselves to this.

Player Control

Okay, you're probably already wondering what it is. PlayerController.
As you might have guessed from the title, PlayerController is an entity that controls the player, namely changes his position and updates his state.

The class definition looks like this:

// PlayerController.h

#pragma once

class Player;

class PlayerController {
private:
    PlayerController() = default;

    static PlayerController* controller;
  
public:

    PlayerController(PlayerController const&) = delete;
    void operator=(PlayerController const&) = delete;
    ~PlayerController();

    static PlayerController* getPlayerController();

    void controllPlayer(Player* player, float time);
};

PlayerController – This singletone class, which means that there will always be only one object of this entity in the program.
To implement this, I made the constructor private, removed the copy constructor, created a static field of the class type, and a static getter that will return that field to us.

The implementation looks like this

// PlayerController.cpp

#include "../include/Engine/PlayerController.h"

#include "../include/Player.h"
#include "../include/Engine/Constants.h"

PlayerController* PlayerController::controller = nullptr;

PlayerController::~PlayerController() {
    delete controller;
}

PlayerController* PlayerController::getPlayerController() {
    if (!controller) {
        controller = new PlayerController();
    }

    return controller;
}

void PlayerController::controllPlayer(Player* player, float time) {
    sf::Vector2f updated_pos = player->getPosition();

    if (sf::Keyboard::isKeyPressed(sf::Keyboard::A)) {
        updated_pos.x -= PLAYER_SPEED * time;
        player->setState(State::RUN);
        player->setDirection(Direction::LEFT);
    }
    else if (sf::Keyboard::isKeyPressed(sf::Keyboard::D)) {
        updated_pos.x += PLAYER_SPEED * time;
        player->setState(State::RUN);
        player->setDirection(Direction::RIGHT);
    }
    if (sf::Keyboard::isKeyPressed(sf::Keyboard::W)) {
        updated_pos.y -= PLAYER_SPEED * time;
        player->setState(State::RUN);
    }
    else if (sf::Keyboard::isKeyPressed(sf::Keyboard::S)) {
        updated_pos.y += PLAYER_SPEED * time;
        player->setState(State::RUN);
    }

    player->setPosition(updated_pos);
}

In a getter, I create an object of the class if it has not already been created and return it.
In method PlayerController::controllPlayer I'm handling keyboard events.
Depending on the key pressed, I change the state and position of the transferred character.

Loading textures

Everything is not complicated here, I’m used to putting all the textures into a separate file, which will contain only one function – setTextures, which will load all textures from the passed paths. Simple and effective, considering what textures you need in a particular game level.

// Textures.h

#pragma once

#include <SFML/Graphics.hpp>

namespace textures {
    sf::Texture player_texture;

    static void setTextures() {
        player_texture.loadFromFile("./Assets/player.jpg");
    }
}

I have only one texture so far, and it’s just a picture, not a tileset, but more on that in the next article.

Finish line

Finally, everything that needs to be written has already been written; all that remains is to connect it all and launch the project.
Final file main.cpp will look something like this:

#include <SFML/Graphics.hpp>

#include "include/Engine/Constants.h"
#include "include/Textures.h"
#include "include/Player.h"

int main() {
    sf::RenderWindow window(sf::VideoMode(WINDOW_WIDTH, WINDOW_HEIGHT), "Title");

    textures::setTextures();

    Player* player = new Player(textures::player_texture, sf::Vector2f(PLAYER_START_X, PLAYER_START_Y), PLAYER_START_HP);

    sf::Clock clock;
    while (window.isOpen()) {
        float time = clock.getElapsedTime().asMicroseconds();
        clock.restart();
        time /= 300;

        sf::Event event;
        while (window.pollEvent(event)) {
            if (event.type == sf::Event::Closed) {
                window.close();
            }
        }

        player->Update(time);

        window.clear(sf::Color::White);

        window.draw(player->getSprite());

        window.display();
    }

    delete player;
    return 0;
}

We have created a class object Playerinitialized it, loaded all the textures and drew the player sprite using window.draw.
Also, do not forget about calling the method Update.

It is worth explaining here about time .
time – this is the current time in microseconds, we pass it to all Update methods, so that the speed of the game did not depend on the frame rate, but on time, thus, on different computers the game will be updated at the same time.

Launch

We build the project, launch it and see the following:

Hooray! everything works, the character moves.
The final file hierarchy looks like this:

Conclusion
Well, the article has come to an end, thanks to everyone who has reached this stage.
In the next part I'll look at creating character animation and creating world boundaries.

Repository with the project – https://github.com/DaniilUbica/toffi – more has been done here than is written in the article, because… I didn’t start writing it right away.

If you have any questions, ask them in the comments, I and the other guys will be happy to answer them

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *