3
\$\begingroup\$

Based on the previous question, I have implemented all suggestions. Also, I worked a bit in this project to improve it.

Summary of improvements:

  • split the code into multiple files as .hpp and .cpp
  • removed STL functions
  • used only one sf::RectangleShape for border instead of std::vector.

I would like to know how I can improve it further.

Utility.hpp

#pragma once

#include <random>


namespace utility
{
    using DistType = std::uniform_real_distribution<float>;

    auto randomEngine();

    float random(DistType& dist);

    float radian(std::size_t direction);

    float degree(std::size_t direction);
}

Utility.cpp

#include "Utility.hpp"

#include <array>
#include <utility>
#include <algorithm>
#include <functional>


auto utility::randomEngine()
{
    std::array<std::mt19937::result_type, std::mt19937::state_size> seed_data;
    thread_local std::random_device source;
    std::generate(std::begin(seed_data), std::end(seed_data), std::ref(source));
    std::seed_seq seeds(std::begin(seed_data), std::end(seed_data));
    thread_local std::mt19937 seeded_engine(seeds);
    return seeded_engine;
}

float utility::random(DistType& dist)
{
    thread_local auto RandomEngine = randomEngine();
    return dist(RandomEngine);
}

float utility::radian(std::size_t direction)
{
    constexpr static auto Ratio = 0.0174532925f; //pi/180
    constexpr static std::array<float, 4> Radians
    {
        180 * Ratio,
        0,
        270 * Ratio,
        90 * Ratio
    };

    return Radians[direction];
}

float utility::degree(std::size_t direction)
{
    constexpr static std::array<float, 4> Degrees
    {
        180,
        0,
        270,
        90
    };

    return Degrees[direction];
}

Snake.hpp

#pragma once

#include <SFML/System/Time.hpp>
#include <SFML/Graphics/Text.hpp>
#include <SFML/System/NonCopyable.hpp>
#include <SFML/Graphics/RectangleShape.hpp>

#include <vector>

namespace sf
{
    class CircleShape;
}


class Snake : public sf::Drawable, sf::NonCopyable
{
public:
    enum Direction
    {
        Up,
        Down,
        Left,
        Right
    };


private:
    using SnakeContainer = std::vector<sf::RectangleShape>;


public:
    explicit Snake(const sf::Font& font, float windowWidth);

    void reset();
    void setDirection(Direction direction);
    float getBlockSize() const;

    bool hasLost() const;
    void lose();

    void extend();
    void update(sf::Time dt);

    sf::FloatRect getGlobalBounds() const;
    bool hasCollideWithFruit(const sf::CircleShape& shape) const;


private:
    void draw(sf::RenderTarget& target, sf::RenderStates states) const override;
    void checkCollision();
    void move(sf::Time dt);
    void shrink(std::size_t index);


private:
    SnakeContainer mSnakeBody;
    Direction mDirection;
    std::size_t mLives;
    sf::Text mLivesText;
    float mBlockSize;
    bool mIsFirstRun;
};

Snake.cpp

#include "Snake.hpp"
#include "Utility.hpp"

#include <SFML/Graphics/CircleShape.hpp>
#include <SFML/Graphics/RenderWindow.hpp>

using namespace utility;


Snake::Snake(const sf::Font& font, float windowWidth) : mBlockSize(16)
{
    reset();

    constexpr auto Offset = 140.f;
    mLivesText.setFont(font);
    mLivesText.setStyle(sf::Text::Bold);
    mLivesText.setCharacterSize(30);
    mLivesText.setColor(sf::Color::White);
    mLivesText.setPosition(windowWidth - Offset, 0.f);
}

void Snake::reset()
{
    mSnakeBody.clear();
    static sf::RectangleShape shape{ { mBlockSize - 1, mBlockSize - 1 } };

    shape.setOrigin(shape.getSize() / 2.f);

    constexpr static auto SnakeLength = 3u;
    constexpr static auto SnakeHead = 0u;
    constexpr static auto SnakeBodyPart = 1u;
    const     static auto Position = sf::Vector2f{ 70, 70 };

    for (auto part = 0u; part < SnakeLength; ++part)
    {
        if (part == SnakeHead)
            shape.setFillColor(sf::Color::Yellow);
        else if (part == SnakeBodyPart)
            shape.setFillColor(sf::Color::Green);

        shape.setPosition(Position.x, Position.y + (SnakeLength - part) * mBlockSize);
        mSnakeBody.emplace_back(shape);
    }

    mDirection = Direction::Down;
    mLives = 3;
    mIsFirstRun = true;
    mLivesText.setString("Lives: " + std::to_string(mLives));
}

void Snake::setDirection(Direction direction)
{
    if (std::abs(degree(mDirection) - degree(direction)) == 180) return;
    if (mIsFirstRun) mIsFirstRun = false;
    mDirection = direction;
}

float Snake::getBlockSize() const
{
    return mBlockSize;
}

bool Snake::hasLost() const
{
    return mLives == 0;
}

void Snake::lose()
{
    mLives = 0;
}

void Snake::extend()
{
    static sf::RectangleShape shape{ { mBlockSize - 1, mBlockSize - 1 } };
    shape.setOrigin(shape.getSize() / 2.f);

    const auto& tail = mSnakeBody.back();

    auto x = tail.getPosition().x - std::sin(radian(mDirection)) * mBlockSize;
    auto y = tail.getPosition().y - std::cos(radian(mDirection)) * mBlockSize;

    shape.setPosition(x, y);
    shape.setFillColor(sf::Color::Green);

    mSnakeBody.emplace_back(shape);
}

void Snake::update(sf::Time dt)
{
    if (mIsFirstRun) return;

    checkCollision();
    move(dt);
}

sf::FloatRect Snake::getGlobalBounds() const
{
    return mSnakeBody.front().getGlobalBounds();
}

bool Snake::hasCollideWithFruit(const sf::CircleShape& shape) const
{
    return std::find_if(mSnakeBody.begin(), mSnakeBody.end(),
        [&shape](const auto& part)
    {
        return part.getGlobalBounds().intersects(shape.getGlobalBounds());
    }) != mSnakeBody.end();
}

void Snake::draw(sf::RenderTarget& target, sf::RenderStates states) const
{
    for (const auto& part : mSnakeBody)
        target.draw(part, states);

    target.draw(mLivesText, states);
}

void Snake::checkCollision()
{
    auto begin = mSnakeBody.cbegin();
    auto end = mSnakeBody.cend();

    for (auto it = std::next(begin); it != end; ++it)
    {
        if (!it->getGlobalBounds().contains(begin->getPosition())) continue;

        auto index = std::distance(begin, it);
        shrink(index);
        break;
    };
}

void Snake::move(sf::Time dt)
{
    constexpr static auto SnakeSpeed = 80.f;

    auto vx = std::sin(radian(mDirection)) * SnakeSpeed * dt.asSeconds();
    auto vy = std::cos(radian(mDirection)) * SnakeSpeed * dt.asSeconds();

    mSnakeBody.front().move(vx, vy);

    auto begin = mSnakeBody.begin();
    auto end = mSnakeBody.end();

    for (auto current = std::next(begin); current != end; ++current)
    {
        auto previous = std::prev(current);

        auto position = previous->getPosition() - current->getPosition();
        auto angle = std::atan2(position.x, position.y);

        auto x = previous->getPosition().x - std::sin(angle) * mBlockSize;
        auto y = previous->getPosition().y - std::cos(angle) * mBlockSize;

        current->setPosition(x, y);
    };
}

void Snake::shrink(std::size_t index)
{
    --mLives;
    auto it = mSnakeBody.begin();
    std::advance(it, index);
    mSnakeBody.erase(it, mSnakeBody.end());
    mLivesText.setString("Lives: " + std::to_string(mLives));
}

World.hpp

#pragma once

#include "Snake.hpp"

#include <SFML/Window/Keyboard.hpp>
#include <SFML/Graphics/CircleShape.hpp>

#include <map>


class World : sf::NonCopyable
{
    using KeysMap = std::map<sf::Keyboard::Key, Snake::Direction>;


public:
    explicit World(sf::RenderTarget& window, const sf::Font& font);

    void update(sf::Time dt);
    void draw();


private:
    void respawnFruit();
    void handleRealTimeInput();
    void checkCollision();
    void checkGameEnd();


private:
    sf::RenderTarget& mWindow;
    Snake mSnake;
    std::size_t mScore;
    sf::RectangleShape mBorder;
    sf::CircleShape mFruit;
    KeysMap mKeyBinding;
    sf::Text mScoreText;
};

World.cpp

#include "World.hpp"
#include "Utility.hpp"

#include <SFML/Graphics/RenderWindow.hpp>

using namespace utility;


World::World(sf::RenderTarget& window, const sf::Font& font)
    : mWindow(window)
    , mSnake(font, static_cast<float>(window.getSize().x))
    , mScore()
{
    const auto blockSize = mSnake.getBlockSize();

    const auto x = window.getSize().x / 2.f;
    const auto y = window.getSize().y / 2.f + blockSize;
    const auto width = window.getSize().x - 2 * blockSize;
    const auto height = window.getSize().y - 4 * blockSize;

    mBorder.setPosition(x, y);
    mBorder.setSize({ width, height });
    mBorder.setOrigin(mBorder.getSize() / 2.f);
    mBorder.setFillColor(sf::Color::Transparent);
    mBorder.setOutlineThickness(-blockSize);
    mBorder.setOutlineColor(sf::Color(211, 211, 211));

    mFruit.setRadius(blockSize / 2.f);
    const auto originX = mFruit.getGlobalBounds().width / 2u;
    const auto originY = mFruit.getGlobalBounds().height / 2u;
    mFruit.setOrigin(originX, originY);
    mFruit.setFillColor(sf::Color::Red);
    respawnFruit();

    // initial keys binding
    mKeyBinding.emplace(sf::Keyboard::Up, Snake::Up);
    mKeyBinding.emplace(sf::Keyboard::Down, Snake::Down);
    mKeyBinding.emplace(sf::Keyboard::Left, Snake::Left);
    mKeyBinding.emplace(sf::Keyboard::Right, Snake::Right);

    mScoreText.setString("Score: " + std::to_string(mScore));
    mScoreText.setFont(font);
    mScoreText.setStyle(sf::Text::Bold);
    mScoreText.setCharacterSize(30);
    mScoreText.setColor(sf::Color::White);
    mScoreText.setPosition(30.f, 0.f);
}

void World::update(sf::Time dt)
{
    handleRealTimeInput();

    checkGameEnd();

    checkCollision();

    mSnake.update(dt);
}

void World::draw()
{
    mWindow.draw(mBorder);
    mWindow.draw(mFruit);
    mWindow.draw(mSnake);
    mWindow.draw(mScoreText);
}

void World::respawnFruit()
{
    const static auto size = mSnake.getBlockSize();
    const static auto bound = mBorder.getGlobalBounds();
    const static auto width = mFruit.getGlobalBounds().width / 2u;
    const static auto height = mFruit.getGlobalBounds().height / 2u;

    static DistType distX(bound.left + size + width, bound.width - width);
    static DistType distY(bound.top + size + height, bound.height + size + height);

    do
        mFruit.setPosition(random(distX), random(distY));
    while (mSnake.hasCollideWithFruit(mFruit));
}

void World::handleRealTimeInput()
{
    for (const auto& pair : mKeyBinding)
    {
        if (sf::Keyboard::isKeyPressed(pair.first))
            mSnake.setDirection(pair.second);
    }
}

void World::checkCollision()
{
    const static auto bound = sf::FloatRect
    {
        mBorder.getGlobalBounds().left + 2 * mSnake.getBlockSize(),
        mBorder.getGlobalBounds().top + 2 * mSnake.getBlockSize(),
        mBorder.getGlobalBounds().width - 4 * mSnake.getBlockSize(),
        mBorder.getGlobalBounds().height - 4 * mSnake.getBlockSize(),
    };

    if (!bound.intersects(mSnake.getGlobalBounds()))
    {
        mScore = 0;
        mScoreText.setString("Score: " + std::to_string(mScore));
        mSnake.lose();
    }

    if (mFruit.getGlobalBounds().intersects(mSnake.getGlobalBounds()))
    {
        mSnake.extend();
        mScore += 10;
        mScoreText.setString("Score: " + std::to_string(mScore));
        respawnFruit();
    }
}

void World::checkGameEnd()
{
    if (mSnake.hasLost())
    {
        mScore = 0;
        mSnake.reset();
        mScoreText.setString("Score: " + std::to_string(mScore));
    }
}

Game.hpp

#pragma once

#include "World.hpp"

#include <SFML/Graphics/RenderWindow.hpp>


class Game : sf::NonCopyable
{
public:
    Game();

    void run();


private:
    void processEvents();
    void update(sf::Time dt);
    void render();


private:
    sf::RenderWindow mWindow;
    World mWorld;
    sf::Font mFont;
};

Game.cpp

#include "Game.hpp"

#include <SFML/Window/Event.hpp>


Game::Game()
    : mWindow(sf::VideoMode(640, 480), "Snake")
    , mWorld(mWindow, mFont)
{
    if (!mFont.loadFromFile("Media/arial.ttf"))
        throw std::runtime_error("Can't load font file");
}

void Game::run()
{
    sf::Clock clock;
    auto timeSinceLastUpdate = sf::Time::Zero;
    const auto TimePerFrame = sf::seconds(1 / 60.f);

    while (mWindow.isOpen())
    {
        auto dt = clock.restart();
        timeSinceLastUpdate += dt;

        while (timeSinceLastUpdate > TimePerFrame)
        {
            timeSinceLastUpdate -= TimePerFrame;

            processEvents();
            update(TimePerFrame);
        }

        render();
    }
}

void Game::processEvents()
{
    sf::Event event;

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

void Game::update(sf::Time dt)
{
    mWorld.update(dt);
}

void Game::render()
{
    mWindow.clear();
    mWorld.draw();
    mWindow.display();
}

Main.cpp

#include "Game.hpp"

#include <stdexcept>
#include <iostream>


int main()
{
    try
    {
        Game game;
        game.run();
    }
    catch (std::runtime_error& e)
    {
        std::cout << "Exception: " << e.what() << std::endl;
        return 1;
    }
}
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.